실제 이미지 데이터를 활용한 CNN 모델 구현하기

실제 이미지로 CNN 모델을 구현하여 나만의 이미지 분류기를 만들 수 있을까?

여러 Youtube 영상과 CNN 관련 코드 예제들을 살펴보면 실제 이미지가 아닌 Tensorflow에서 제공하는 MNIST 데이터를 활용한 것을 알 수 있습니다. Tensorflow에서 제공한 MNIST 데이터는 실제 이미지가 아닌 숫자 데이터이며, 이 때문에 실제 이미지를 활용하여 CNN 모델을 구현하는 것이 쉽지 않습니다.

<Figure 1> Tensorflow에서 제공하는 MNIST 데이터
실제 이미지가 아닌 ubyte형태로 제공됨

본 게시글에서는 Tensorflow에서 제공하는 숫자로 이루어진 MNIST 데이터가 아닌 실제 이미지로 이루어진 MNIST 데이터를 활용하여 CNN 모델을 구현하고자 합니다.

CNN 모델 구현 절차는 다음과 같습니다.

  1. 이미지를 숫자형 데이터로 변환
  2. 모델 설계
  3. 모델 학습
  4. 모델 정확도 산출

1. 이미지를 숫자형 데이터로 변환

우선 아래의 링크를 클릭하여 MNIST 데이터를 다운로드 합니다.

MNIST.zip (248 downloads)

다운로드 받은 파일을 압축 해제하면 ‘testSet’ 폴더와 ‘trainingSet’폴더가 있는 것을 확인할 수 있으며, 각 폴더 내에는 0~9까지의 개별 폴더들이 있고, 개별 폴더 내에는 숫자 이미지가 있는 것을 확인할 수 있습니다.

trainingSet 폴더 내에는 총 42,000개의 0~9까지의 image 데이터가 있으며, testSet에는 총 180개의 0~9까지의 image 데이터가 있습니다. CNN 모델의 input으로 이미지 데이터를 활용하기 위해 이미지 데이터를 숫자형 데이터로 변환하겠습니다.

이미지 데이터를 숫자형 데이터로 변환하기 위해 필요한 파이썬 라이브러리는 다음과 같습니다.

  1. os
  2. cv2
  3. numpy
  4. sklearn

pip를 활용하여 위의 파이썬 라이브러리를 다운받을 수 있으며, cmd를 활용한 라이브러리 다운로드 명령문 예제는 다음과 같습니다.

pip3 install os

다음은 train 이미지 데이터를 숫자로 변환하고, 각 이미지 데이터의 라벨 데이터를 추출하는 python3 코드입니다. 


import os
import cv2
import numpy as np
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import OneHotEncoder
from numpy import array

TRAIN_DIR = 'D:/python3_project/MNIST_CNN/trainingSet/'
train_folder_list = array(os.listdir(TRAIN_DIR))

train_input = []
train_label = []

label_encoder = LabelEncoder()  # LabelEncoder Class 호출
integer_encoded = label_encoder.fit_transform(train_folder_list)
onehot_encoder = OneHotEncoder(sparse=False) 
integer_encoded = integer_encoded.reshape(len(integer_encoded), 1)
onehot_encoded = onehot_encoder.fit_transform(integer_encoded)

for index in range(len(train_folder_list)):
    path = os.path.join(TRAIN_DIR, train_folder_list[index])
    path = path + '/'
    img_list = os.listdir(path)
    for img in img_list:
        img_path = os.path.join(path, img)
        img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
        train_input.append([np.array(img)])
        train_label.append([np.array(onehot_encoded[index])])

train_input = np.reshape(train_input, (-1, 784))
train_label = np.reshape(train_label, (-1, 10))
train_input = np.array(train_input).astype(np.float32)
train_label = np.array(train_label).astype(np.float32)
np.save("train_data.npy", train_input)
np.save("train_label.npy", train_label)

코드에 대한 설명은 다음과 같습니다.

<1~6번째 줄 code 설명>
import os
: 주로 디렉토리 경로를 호출할때 사용됩니다
import cv2 : 이미지 파일을 불러올 때 사용됩니다.
import numpy as np : 데이터 처리에 자주 사용되는 라이브러리이며, 다양한 행렬 연산 기능을 제공합니다.
from numpy import array
: numpy 라이브러리에서 array라는 함수를 불러옵니다. 리스트를 array 형태로 만들때 사용됩니다.
from sklearn.preprocessing import LabelEncoder
: sklearn.preprocessing에서 LabelEncoder 함수를 불러옵니다. 본 코드에서는 문자로된 폴더 리스트를 숫자형 array로 변환할때 사용합니다.
from sklearn.preprocessing import OneHotEncode
: one-hot-encoding을 위해 OneHotEncoder 함수를 불러옵니다. 본 코드에서는 숫자형 array를 one-hot-encode할 때 사용됩니다.

<8,9번째 줄 code 설명>
TRAIN_DIR = ‘D:/python3_project/MNIST_CNN/MNIST/trainingSet/’
: train 이미지 데이터셋의 경로를 지정합니다.
train_folder_list = array(os.listdir(TRAIN_DIR)) : trainingSet 내부에 존재하는 폴더명을 array 형태로 저장합니다. train_folder_list를 print하면 다음과 같이 출력됩니다.

['0_zero' '1_one' '2_two' '3_three' '4_four' '5_five' '6_six' '7_seven' '8_eight' '9_nine']

<14~18번째 줄 code 설명>
train_input = []
train_label = []
: train_input이라는 변수에 빈 리스트를 할당하며, train_label이라는 변수에 빈 리스트를 할당합니다.
label_encoder = LabelEncoder() : 앞서 from sklearn.preprocessing import LabelEncoder를 통해 불러온 LabelEncoder를 label_encoder라는 변수로 호출합니다.
integer_encoded = label_encoder.fit_transform(train_folder_list)
: 문자열로 구성된 train_folder_list를 숫자형 리스트로 변환합니다. integer_encoded를 print하면 다음과 같이 출력됩니다.

[0 1 2 3 4 5 6 7 8 9]

onehot_encoder = OneHotEncoder(sparse=False) : 앞서 from sklearn.preprocessing import OneHotEncoder를 통해 불러온 OneHotEncoder 함수를 onehot_encoder라는 변수로 호출합니다.
integer_encoded = integer_encoded.reshape(len(integer_encoded), 1) : OneHotEncoder를 사용하기 위해 integer_encoded의 shape을 (10,)에서 (10,1)로 변환합니다. integer_encoded를 출력하면 다음과 같습니다.

[[0] [1] [2] [3] [4] [5] [6] [7] [8] [9]]

onehot_encoded = onehot_encoder.fit_transform(integer_encoded) : OneHotEncoder를 사용하여 integer_encode를 다음과 같이 변환하여 onehot_encoded 변수에 저장합니다.

[[ 1.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  1.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  1.]]

<20~23번째 줄 code 설명>
for index in range(len(train_folder_list)):
: for문을 만듭니다. index라는 변수가 0에서 9변할 때 까지 for문이 실행됩니다.
path = os.path.join(TRAIN_DIR, train_folder_list[index])
: path라는 변수에 TRAIN_DIR(D:/python3_project/MNIST_CNN/MNIST/trainingSet/)과 train_folder_list의 n번째 원소를 합쳐서 path에 저장합니다. path 출력 예시는 다음과 같습니다.

'D:/python3_project/MNIST_CNN/trainingSet/0_zero'

path = path + ‘/’ : path라는 변수에 ‘/’를 추가합니다. path 출력 예시는 다음과 같습니다.

'D:/python3_project/MNIST_CNN/trainingSet/0_zero/'

img_list = os.listdir(path) : img_list 변수에 폴더 내 파일명을 저장합니다. img_list 출력 예시는 다음과 같습니다.

['img_1.jpg' 'img_3.jpg' 'img_10.jpg' ... 'img_n.jpg']

<24~28번째 줄 code 설명>
for img in img_list:
img_path = os.path.join(path, img) : img_path에 정확한 이미지 경로를 저장합니다. img_path 출력 예시는 다음과 같습니다.

'D:/python3_project/MNIST_CNN/trainingSet/0_zero/img_1.jpg'

img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) : img 변수에 cv2.imread를 활용하여 path경로에 있는 이미지를 흑백으로 불러옵니다. img 출력 예시는 다음과 같습니다.

[[3   0   0   3   7   3   0   3   0  11   0   0   3   0   0   3   8   0   0   3   0   0   0   2   0   0   0   0]
 [0   0   0   0   0   0   0   1   5   0  12   0  16   0   0   4   0   2   8   3   0   4   8   0   0   0   0   0]
 [0   0   2   0   0   0   1   2   1  12   0   8   0   0   6   0  11   0   0   6   7   2   0   0   0   0   0   0]
 [0   1   3   0   0   2   3   0   0   0  12   0   0  23   0   0   0   0  11   3   0   0   4   0   0   0   0   0]
 [0   1   1   0   0   2   0   0   6   0  25  27 136 135 188  89  84  25   0   0   3   1   0   0   0   0   0   0]
 [4   0   0   0   0   0   0   0   3  88 247 236 255 249 250 227 240 136  37   1   0   2   2   0   0   0   0   0]
 [2   0   0   3   0   0   4  27 193 251 253 255 255 255 255 240 254 255 213  89   0   0  14   1   0   0   0   0]
 [0   0   0   6   0   0  18  56 246 255 253 243 251 255 245 255 255 254 255 231 119   7   0   5   0   0   0   0]
 [4   0   0  12  13   0  65 190 246 255 255 251 255 109  88 199 255 247 250 255 234  92   0   0   0   0   0   0]
 [0  10   1   0   0  18 163 248 255 235 216 150 128  45   6   8  22 212 255 255 252 172   0  15   0   0   0   0]
 [0   1   4   5   0   0 187 255 254  94  57   7   1   0   6   0   0 139 242 255 255 218  62   0   0   0   0   0]
 [5   2   0   0  11  56 252 235 253  20   5   2   5   1   0   1   2   0  97 249 248 249 166   8   0   0   0   0]
 [0   0   2   0   0  70 255 255 245  25  10   0   0   1   0   4  10   0  10 255 246 250 155   0   0   0   0   0]
 [2   0   7  12   0  87 226 255 184   0   3   0  10   5   0   0   0   9   0 183 251 255 222  15   0   0   0   0]
 [0   5   1   0  19 230 255 243 255  35   2   0   0   0   0   9   8   0   0  70 245 242 255  14   0   0   0   0]
 [0   4   3   0  19 251 239 255 247  30   1   0   4   4  14   0   0   2   0  47 255 255 247  21   0   0   0   0]
 [6   0   2   2   0 173 247 252 250  28  10   0   0   8   0   0   0   8   0  67 249 255 255  12   0   0   0   0]
 [0   0   6   3   0  88 255 251 255 188  21   0  15   0   8   2  16   0  35 200 247 251 134   4   0   0   0   0]
 [0   3   3   1   0  11 211 247 249 255 189  76   0   0   4   0   2   0 169 255 255 247  47   0   0   0   0   0]
 [0   6   0   0   2   0  59 205 255 240 255 182  41  56  28  33  42 239 246 251 238 157   0   1   0   0   0   0]
 [2   1   0   0   2  10   0 104 239 255 240 255 253 247 237 255 255 250 255 239 255 100   0   1   0   0   0   0]
 [1   0   3   0   0   7   0   4 114 255 255 255 255 247 249 253 251 254 237 251  89   0   0   1   0   0   0   0]
 [0   0   9   0   0   1  13   0  14 167 255 246 253 255 255 254 242 255 244  61   0  19   0   1   0   0   0   0]
 [2   1   7   0   0   4   0  14   0  27  61 143 255 255 252 255 149  21   6  16   0   0   7   0   0   0   0   0]
 [0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]]

train_input.append([np.array(img)]) : train_input이라는 리스트에 img를 np.array 형식으로 붙여넣습니다. .append는 R에서의 rbind와 같은 명령문입니다.
train_label.append([np.array(onehot_encoded[index])]) : train_label이라는 리스트에 onehot_encoded의 index번째 행을 np.array 형식으로 붙여넣습니다. 예를들어, index가 0일 때는 [1 0 0 0 0 0 0 0 0 0]을, 3일 때는 [0 0 0 1 0 0 0 0 0 0 0]을 붙여넣습니다.

<30~35번째 줄 code 설명>
train_input = np.reshape(train_input, (-1, 784)) :
현재 train_input은 list 형태이므로 shape이 (-1, 784)인 형태의 np.array로 변환합니다. (-1, 784)에서 -1은 데이터의 정확한 개수를 모를 때 사용하는 숫자이며 784는 이미지의 형태가 28*28이므로 정사각형 모양의 데이터 형태를 1자 형태로 바꾸기 위해 28의 제곱인 784를 사용합니다. 코드 실행 후 train_input의 shape은 다음과 같이 출력됩니다.

(42000,784)

즉, train_input에는 42,000개의 데이터가 존재하며, 각 데이터는 784개의 숫자로 이루어져있음을 알 수 있습니다. 
train_label = np.reshape(train_label, (-1, 10))
: train_input과 마찬가지로 train_label또한 list 형태이므로 (-1,10)형태의 np.array로 변환합니다. 코드 실행 후 train_label의 shape은 다음과 같이 출력됩니다.

(42000,10)

즉, train_label에는 42,000개의 데이터가 존재하며, 각 데이터는 10개의 숫자로 이루어져있음을 알 수 있습니다. 
train_input = np.array(train_input).astype(np.float32)
train_label = np.array(train_label).astype(np.float32)
: train_input과 train_label의 데이터 타입을 float32로 변환합니다. 
np.save(“train_data.npy”, train_input)
np.save(“train_label.npy”, train_label)
: 최종적으로 변환된 train_input과 train_label을 각각 train_data.npy와 train_label.npy로 저장합니다. 

최종적으로 train_input과 train_label에 저장된 데이터를 추상화하여 보면 다음 그림과 같습니다.

데이셋에 shuffle을 수행하지 않았기 때문에 0~9까지 순차적으로 데이터가 저장된 것을 알 수 있습니다. 데이터셋의 형태를 잘 기억하시길 바랍니다.

지금까지 train 이미지 데이터를 숫자 형태로 변환하고 각 이미지의 라벨을 만들어 보았습니다. 이제 test 이미지 데이터를 train 이미지 데이터와 같이 숫자 형태로 변환하고 각 이미지의 라벨을 만들어야 합니다. test 이미지 데이터에 대한 숫자 변환 과정은 train 이미지 데이터를 숫자 형태로 변환하는 과정과 동일하며, code는 다음과 같습니다.

TEST_DIR = 'D:/python3_project/MNIST_CNN/testSet/'
test_folder_list = array(os.listdir(TEST_DIR))

test_input = []
test_label = []

label_encoder = LabelEncoder()
integer_encoded = label_encoder.fit_transform(test_folder_list)

onehot_encoder = OneHotEncoder(sparse=False) 
integer_encoded = integer_encoded.reshape(len(integer_encoded), 1)
onehot_encoded = onehot_encoder.fit_transform(integer_encoded)

for index in range(len(test_folder_list)):
    path = os.path.join(TEST_DIR, test_folder_list[index])
    path = path + '/'
    img_list = os.listdir(path)
    for img in img_list:
        img_path = os.path.join(path, img)
        img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
        test_input.append([np.array(img)])
        test_label.append([np.array(onehot_encoded[index])])

test_input = np.reshape(test_input, (-1, 784))
test_label = np.reshape(test_label, (-1, 10))
test_input = np.array(test_input).astype(np.float32)
test_label = np.array(test_label).astype(np.float32)
np.save("test_input.npy",test_input)
np.save("test_label.npy",test_label)

2. 모델 설계

이미지 데이터를 숫자형 데이터로 변환했으므로, 이미지를 분류하는 CNN 모델을 설계해야 합니다. 본 게시글에서 사용하는 CNN 모델은 아래 그림과 같이 비교적 단순한 모델이며, ‘모두의 머신러닝’ 강의를 진행하신 김성훈 교수님의 코드를 차용하겠습니다.

import tensorflow as tf

# hyper parameters
learning_rate = 0.001

# input place holders
X = tf.placeholder(tf.float32, [None, 784])
X_img = tf.reshape(X, [-1, 28, 28, 1])   # img 28x28x1 (black/white)
Y = tf.placeholder(tf.float32, [None, 10])

W1 = tf.Variable(tf.random_normal([3, 3, 1, 32], stddev=0.01))
L1 = tf.nn.conv2d(X_img, W1, strides=[1, 1, 1, 1], padding='SAME')
L1 = tf.nn.relu(L1)
L1 = tf.nn.max_pool(L1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

W2 = tf.Variable(tf.random_normal([3, 3, 32, 64], stddev=0.01))
L2 = tf.nn.conv2d(L1, W2, strides=[1, 1, 1, 1], padding='SAME')
L2 = tf.nn.relu(L2)
L2 = tf.nn.max_pool(L2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

L2_flat = tf.reshape(L2, [-1, 7 * 7 * 64])
W3 = tf.get_variable("W3", shape=[7 * 7 * 64, 10], initializer=tf.contrib.layers.xavier_initializer())
b = tf.Variable(tf.random_normal([10]))
logits = tf.matmul(L2_flat, W3) + b

# define cost/loss &amp;amp;amp; optimizer
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
    logits=logits, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

출처 : https://github.com/hunkim/DeepLearningZeroToAll/blob/master/lab-11-3-mnist_cnn_class.py

<1~21번째 줄 code 설명>

import tensorflow as tf : tensorflow 라이브러리를 불러옵니다.

learning_rate = 0.001 : learning rate를 설정합니다.

X = tf.placeholder(tf.float32, [None, 784]) : input data를 불러올 변수를 선언합니다.
tf.placeholder : 재료를 담는 그릇 또는 쟁반이라고 생각하면 됩니다. tf.placeholder를 통해 input 변수와 output 변수를 불러올 수 있습니다. tf.float32라는 것은 변수의 데이터 타입을 float32로 지정한 것이며, [None, 784]는 784의 shape을 갖는 데이터를 0~무한대 까지 불러올 수 있다는 뜻입니다.
X_img = tf.reshape(X, [-1, 28, 28, 1]) : X라는 변수의 shape은 [-1, 784]이므로 원래 데이터의 shape([-1, 28, 28, 1)]으로 바꾸어  X_img에 저장합니다. [-1, 28, 28, 1]에서 -1은 batch size를 뜻합니다. batch size는 가변할 수 있으므로 대부분 -1로 지정합니다. 28, 28은 이미지의 너비와 높이를 의미합니다. 마지막의 1은 이미지의 channel 수를 의미합니다. grayscale로 이미지를 불러왔으므로 1로 설정되었으며, RGB로 불러왔을 경우에는 3을 기입해야합니다.
Y = tf.placeholder(tf.float32, [None, 10]) : output data를 불러올 변수를 선언합니다. label의 shape이 [-1, 10]이므로 [None, 10]을 기입합니다.

W1 = tf.Variable(tf.random_normal([3, 3, 1, 32], stddev=0.01)) : Convolution layer의 필터 크기와 개수를 선언하여 W1에 저장합니다. tf.random_normal[3, 3, 1, 32]에서 tf.random_normal은 정규분포에서 난수를 추출하여 저장한다는 의미이며, 3,3은 필터 크기 즉, 3*3필터를 쓰겠다고 선언한 것이며, 1은 input data의 channel인 1을 지정했습니다. 마지막 32는 3*3필터를 총 32개 쓰겠다고 선언한 것입니다. stddev=0.01은 생성된 난수의 변동이 0.01이라는 뜻입니다(몰라도 상관 없습니다. 그냥 stddev=0.01을 보통 씁니다.)
L1 = tf.nn.conv2d(X_img, W1, strides=[1, 1, 1, 1], padding=’SAME’) : Convoluton layer를 선언하여 L1에 저장합니다. X_img는 해당 Convolution layer의 input이며, W1은 앞서 선언한 Convolution layer의 필터입니다. 즉, X_img에 W1 필터를 활용하여 Convolution layer를 구성하겠다는 뜻입니다.
strides=[1,1,1,1]은 stride를 어떻게 움직일 것인가에 대해 설정하는 구문입니다. 첫번째의 1은 모든 batch에 대해 convolution filter를 적용하겠다는 의미이며, 마지막의 1은 모든 채널에 대해 convoultion filter를 적용하겠다는 의미입니다. 두번째와 세번째의 1은 필터를 움직일 때 우측으로 한칸 씩, 아래로 한칸 씩 움직인다는 의미입니다.
padding=’SAME’은 convolution 연산 후 shape이 줄어드는 것을 방지하기 위하여 설정하는 구문입니다. 만약 padding=’VALID’로 설정한다면, 3*3필터가 28*28이미지를 한칸 씩 움직이며 연산을 수행하므로 convoultion 연산 이후 출력된 결과의 shape은 28*28이 아닌 26*26이 됩니다.
L1 = tf.nn.relu(L1) : Convolution layer의 연산 결과를 RELU activation fuction에 적용하겠다는 의미입니다.
L1 = tf.nn.max_pool(L1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding=’SAME’) : max-pooling layer를 선언하여 L1에 저장합니다. max-pooling layer의 입력 변수는 이전에 선언된 L1입니다.
ksize=[1,2,2,1]에 대한 설명은 다음과 같습니다. ksize는 kernel 사이즈를 의미하며, Conolution layer의 filter와 동일한 개념이라고 생각하면 됩니다. 첫번째 1은 모든 batch에 대하여 kernel을 적적용하겠다는 의미이며, [2,2]는 2*2크기의 kernel을 사용하겠다는 의미입니다. 마지막 1은 모든 채널에 대하여 kernel을 적용하겠다는 의미입니다.
strides=[1,2,2,1]에서 kernel과 같이 [1,2,2,1]를 입력했는데 이유는 다음과 같습니다. kernel 사이즈와 stride 크기는 보통 같게 설정합니다. 예를들어 2*2의 kernel을 사용한다면 오른쪽과 아래로 2칸씩 움직이며, kernel 사이즈가 3*3이면 오늘쪽과 아래로 3칸씩 움직이는 것이 일반적입니다.

W2,L2는 위의 과정과 동일한 과정으로 만들어 졌으므로 자세한 설명은 생략하겠습니다.

<23~26번째 줄 code 설명>

L2_flat = tf.reshape(L2, [-1, 7 * 7 * 64]) : L2의 shape을 [-1, 7*7*64]로 만들어 L2_flat에 저장합니다.
[7*7*64]인 이유는 다음과 같습니다.
처음 input data의 shape은 28*28*1이었습니다. input data가 convoultion layer(L1)을 거쳐 32개의 3*3의 필터와 연산되었으므로 이 때의 연산 결과는 28*28*32가 됩니다(padding=’SAME’으로 지정하였으므로 data의 크기는 변하지 않습니다). L1은 max-pooling layer(L1)를 거쳐 2*2의 kernel으로 max-pooling 연산을 수행했으므로 이 때의 연산 결과는 14*14*32가 됩니다. 마찬가지로 convolution layer(L2)에서 3*3*64 연산을 수행하면 14*14*64가 되며 max-pooling(L2)에서 2*2 kernel으로 max-pooling 연산을 수행하면 7*7*64가 됩니다.

W3 = tf.get_variable(“W3”, shape=[7 * 7 * 64, 10], initializer=tf.contrib.layers.xavier_initializer()) : fully-connected 연산을 위해 weight를 선언하여 W3에 저장합니다. 이 때, W3의 shape을 [7*7*64, 10]으로 지정했는데, input data의 shape이 7*7*64이며, output data의 shape이 10(0~9)이기 때문에 W3의 shape을 [7*7*64, 10]으로 설정합니다. Weigth의 초기값은 성능이 우수한 것으로 알려진 Xavier initializer를 사용합니다.
b = tf.Variable(tf.random_normal([10])) : bias를 선언하여 b에 저장합니다. output의 shape이 10이므로 shape을 10으로 설정합니다.
logits = tf.matmul(L2_flat, W3) + b : L2_flat과 W3 행렬의 곱셈 연산을 수행한 후 bias를 더하여 logits에 저장합니다((W3 * L2_flat) + b).

<29~31번째 줄 code 설명>

cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
logits=logits, labels=Y)) : 예측 값(logtis)과 실제 값(Y)의 차이를 나타내는 Cost함수를 선언하여 cost에 저장합니다.
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost) : cost를 최소화하는 optimizer 함수를 선언합니다.

3. 모델 학습

모델을 설계하였으므로, 이미지 분류기를 만들기 위해 모델의 학습을 수행해야 합니다. 모델 학습 코드 또한 ‘모두의 머신러닝’ 강의를 진행하신 김성훈 교수님의 코드를 차용하겠습니다.

training_epochs = 15
batch_size = 100

# initialize
sess = tf.Session()
sess.run(tf.global_variables_initializer())

# train my model
print('Learning started. It takes sometime.')
for epoch in range(training_epochs):
    avg_cost = 0
    total_batch = int(len(train_input) / batch_size)

    for i in range(total_batch):
        start = ((i+1) * batch_size) - batch_size
        end = ((i+1) * batch_size)
        batch_xs = train_input[start:end]
        batch_ys = train_label[start:end]
        feed_dict = {X: batch_xs, Y: batch_ys}
        c, _ = sess.run([cost, optimizer], feed_dict=feed_dict)
        avg_cost += c / total_batch

    print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.9f}'.format(avg_cost))

print('Learning Finished!')

출처 : https://github.com/hunkim/DeepLearningZeroToAll/blob/master/lab-11-3-mnist_cnn_class.py

<1~2번째 줄 code 설명>

training_epochs = 15 : 학습 횟수를 설정합니다.
batch_size = 100 : 효과적인 모델 학습을 위해 batch size를 설정합니다. batch size는 학습 할 때 몇개의 데이터를 한번에 학습하는가에  관한 설정입니다. 본 실험에서는 42,000개의 데이터를 학습하므로, batch size는 1~42,000까지 설정할 수 있습니다. 100으로 설정했으므로 한번 학습하는데 100개의 데이터를 사용한다는 의미입니다.

<5~6번째 줄 code 설명>

sess = tf.Session() : tf.Session 클래스를 sess에 저장합니다.
sess.run(tf.global_variables_initializer()) : 모든 변수의 weight값을 초기화 합니다.

<10~12번째 줄 code 설명>

for epoch in range(training_epochs): :반복문을 선언합니다.
avg_cost = 0 : 1 epoch 완료 시 cost의 평균값을 출력하기 위해 avg_cost를 선언한 후 0을 저장합니다.
total_batch = int(len(train_data) / batch_size) : 1 epoch에 몇회 학습할 것인지를 설정합니다. train_input에 저장된 데이터는 42,000개이므로 len(train_input)은 42,000을, batch_size는 100으로 설정했으므로 total_batch는 420이 됩니다.

<14~21번째 줄 code 설명>

for i in range(total_batch): : 반복문을 선언합니다.
start = ((i+1) * batch_size) – batch_size : 데이터를 분할하기 위해 start라는 변수를 선언합니다. i는 0에서 419까지 변합니다. i가 0일 때 start에 저장되는 값은 ((0+1)*100)-100이므로 100이 저장됩니다.
end = ((i+1) * batch_size) : 데이터를 분할하기 위해 end라는 변수를 선언합니다. i가 0일 때, ((0+1)*100)이므로 end에는 100이 저장됩니다.
batch_xs = train_input[start:end] : batch_xs는 train 데이터의 input을 저장하는 변수입니다. train_input[start:end]는 train_input[0:100]과 같으므로 train_input에서 0~100에 위치하는 데이터를 불러서 batch_xs에 저장합니다.
batch_ys = train_label[start:end] : batch_ys는 train 데이터의 label을 저장하는 변수입니다. train_label[start:end]는 train_label[0:100]과 같으므로 train_label에서 0~100에 위치하는 데이터를 불러 batch_ys에 저장합니다.
feed_dict = {X: batch_xs, Y: batch_ys} : feed dictionary를 선언합니다. X는 input data에 대한 placeholder이며, Y는 output data 즉, label을 담는 placeholder입니다. X:batch_xs는 X에 batch_xs를 담겠다는 뜻이며, y:batch_ys는 Y에 batch_ys를 담겠다는 의미입니다.
c, _ = sess.run([cost, optimizer], feed_dict=feed_dict) : Session을 실행합니다. cost와  optimizer를 실행한 후 c에 cost를 저장합니다.
avg_cost += c / total_batch : avg_cost에 (c / total_batch)를 더합니다. 즉 cost의 평균을 avg_cost에 더하여 저장합니다.

5. 모델 정확도 산출

모델 학습을 수행했으므로, 이미지 분류기의 성능을 측정합니다. 모델 정확도 산출 코드 또한 ‘모두의 머신러닝’ 강의를 진행하신 김성훈 교수님의 코드를 차용하겠습니다.

# Test model and check accuracy
correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
print('Accuracy:', sess.run(accuracy, feed_dict={
      X: test_input, Y: test_label}))

correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(Y, 1)) : 전체 test 데이터 중 실제로 맞춘 개수가 몇개인지 측정한 다음 correct_prediction에 저장합니다. tf.equal에서는 예측 값과 정답이 같으면 True 아니면 False 값이 반환되는데, 이것을 float형으로 바꾸고 평균을 계산해 정확도를 구합니다. tf.argmax 함수는 텐서 내의 지정된 축에서 가장 높은 값의 인덱스를 반환합니다.
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) : 정확도의 평균을 구하여 accuracy에 저장합니다.
print(‘Accuracy:’, sess.run(accuracy, feed_dict={X: test_input, Y: test_label})) : Accuracy를 출력합니다. feed_dict={X: test_input, Y: test_label}은 X에 test_input을, Y에 test_label을 불러오겠다는 것을 의미합니다.

지금까지 완성된 code는 다음과 같습니다.

 


import os
import cv2
import numpy as np
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import OneHotEncoder
from numpy import array
import tensorflow as tf

TRAIN_DIR = 'D:/python3_project/MNIST_CNN/trainingSet/'
train_folder_list = array(os.listdir(TRAIN_DIR))

train_input = []
train_label = []

label_encoder = LabelEncoder()  # LabelEncoder Class 호출
integer_encoded = label_encoder.fit_transform(train_folder_list)
onehot_encoder = OneHotEncoder(sparse=False) 
integer_encoded = integer_encoded.reshape(len(integer_encoded), 1)
onehot_encoded = onehot_encoder.fit_transform(integer_encoded)

for index in range(len(train_folder_list)):
    path = os.path.join(TRAIN_DIR, train_folder_list[index])
    path = path + '/'
    img_list = os.listdir(path)
    for img in img_list:
        img_path = os.path.join(path, img)
        img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
        train_input.append([np.array(img)])
        train_label.append([np.array(onehot_encoded[index])])

train_input = np.reshape(train_input, (-1, 784))
train_label = np.reshape(train_label, (-1, 10))
train_input = np.array(train_input).astype(np.float32)
train_label = np.array(train_label).astype(np.float32)
np.save("train_data.npy", train_input)
np.save("train_label.npy", train_label)

TEST_DIR = 'D:/python3_project/MNIST_CNN/testSet/'
test_folder_list = array(os.listdir(TEST_DIR))

test_input = []
test_label = []

label_encoder = LabelEncoder()
integer_encoded = label_encoder.fit_transform(test_folder_list)

onehot_encoder = OneHotEncoder(sparse=False) 
integer_encoded = integer_encoded.reshape(len(integer_encoded), 1)
onehot_encoded = onehot_encoder.fit_transform(integer_encoded)

for index in range(len(test_folder_list)):
    path = os.path.join(TEST_DIR, test_folder_list[index])
    path = path + '/'
    img_list = os.listdir(path)
    for img in img_list:
        img_path = os.path.join(path, img)
        img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
        test_input.append([np.array(img)])
        test_label.append([np.array(onehot_encoded[index])])

test_input = np.reshape(test_input, (-1, 784))
test_label = np.reshape(test_label, (-1, 10))
test_input = np.array(test_input).astype(np.float32)
test_label = np.array(test_label).astype(np.float32)
np.save("test_input.npy",test_input)
np.save("test_label.npy",test_label)

# hyper parameters
learning_rate = 0.001

# input place holders
X = tf.placeholder(tf.float32, [None, 784])
X_img = tf.reshape(X, [-1, 28, 28, 1])   # img 28x28x1 (black/white)
Y = tf.placeholder(tf.float32, [None, 10])

W1 = tf.Variable(tf.random_normal([3, 3, 1, 32], stddev=0.01))
L1 = tf.nn.conv2d(X_img, W1, strides=[1, 1, 1, 1], padding='SAME')
L1 = tf.nn.relu(L1)
L1 = tf.nn.max_pool(L1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

W2 = tf.Variable(tf.random_normal([3, 3, 32, 64], stddev=0.01))
L2 = tf.nn.conv2d(L1, W2, strides=[1, 1, 1, 1], padding='SAME')
L2 = tf.nn.relu(L2)
L2 = tf.nn.max_pool(L2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

L2_flat = tf.reshape(L2, [-1, 7 * 7 * 64])
W3 = tf.get_variable("W3", shape=[7 * 7 * 64, 10], initializer=tf.contrib.layers.xavier_initializer())
b = tf.Variable(tf.random_normal([10]))
logits = tf.matmul(L2_flat, W3) + b

# define cost/loss &amp;amp;amp; optimizer
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
    logits=logits, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

training_epochs = 15
batch_size = 100

# initialize
sess = tf.Session()
sess.run(tf.global_variables_initializer())

# train my model
print('Learning started. It takes sometime.')
for epoch in range(training_epochs):
    avg_cost = 0
    total_batch = int(len(train_input) / batch_size)

    for i in range(total_batch):
        start = ((i + 1) * batch_size) - batch_size
        end = ((i + 1) * batch_size)
        batch_xs = train_input[start:end]
        batch_ys = train_label[start:end]
        feed_dict = {X: batch_xs, Y: batch_ys}
        c, _ = sess.run([cost, optimizer], feed_dict=feed_dict)
        avg_cost += c / total_batch

    print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.9f}'.format(avg_cost))

print('Learning Finished!')

# Test model and check accuracy
correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
print('Accuracy:', sess.run(accuracy, feed_dict={
      X: test_input, Y: test_label}))

모든 코드가 완성되었습니다. 자 이제 학습을 수행하고 이미지 분류기의 정확도를 산출해보겠습니다.

<code 실행 결과>

Learning started. It takes sometime.
Epoch: 0001 cost = 2.719625452
Epoch: 0002 cost = 2.287379142
Epoch: 0003 cost = 2.367383643
Epoch: 0004 cost = 2.616217432
Epoch: 0005 cost = 2.561655218
Epoch: 0006 cost = 2.518745302
Epoch: 0007 cost = 2.483943181
Epoch: 0008 cost = 2.455336698
Epoch: 0009 cost = 2.431678627
Epoch: 0010 cost = 2.412068066
Epoch: 0011 cost = 2.395810360
Epoch: 0012 cost = 2.382348032
Epoch: 0013 cost = 2.371222212
Epoch: 0014 cost = 2.362049607
Epoch: 0015 cost = 2.354507778
Learning Finished!
Accuracy: 0.1

??? 이게 무슨일일까요 ???

분명 김성훈 교수님께서 작성하신 코드를 사용했고, 데이터도 MNIST 데이터를 활용했는데 Cost는 0점대로 떨어지지 않고, 모델 정확도는 10%밖에 나타나지 않습니다. 모델이 잘못된 걸까요? 데이터가 잘못된걸까요?

아래는 김성훈 교수님께서 작성하신 코드를 그대로 실행한 결과입니다.

<code 실행 결과>

Epoch: 0001 cost = 0.340291267
Epoch: 0002 cost = 0.090731326
Epoch: 0003 cost = 0.064477619
Epoch: 0004 cost = 0.050683064
Epoch: 0005 cost = 0.041864835
Epoch: 0006 cost = 0.035760704
Epoch: 0007 cost = 0.030572132
Epoch: 0008 cost = 0.026207981
Epoch: 0009 cost = 0.022622454
Epoch: 0010 cost = 0.019055919
Epoch: 0011 cost = 0.017758641
Epoch: 0012 cost = 0.014156652
Epoch: 0013 cost = 0.012397016
Epoch: 0014 cost = 0.010693789
Epoch: 0015 cost = 0.009469977
Learning Finished!
Accuracy: 0.9885

똑같은 모델로 구성되어있기 때문에 모델이 문제인것 같지는 않습니다. 그럼 데이터에 문제가 있다는 의미인데 데이터에 어떤 문제가 있을까요??

제가 정답을 말씀드리면 가장 중요한 사실을 너무 가볍게 말씀드리는 것이기 때문에, 해당 문제에 대한 정답은 숙제로 남겨두겠습니다. (사실, 이미 말씀드리기도 했습니다. 잘 찾아보세요)

댓글 남기기