갈루아의 반서재

Create your first Image Recognition Classifier using CNN, Keras and Tensorflow backend


Getting Started — Dog or Cat


본 튜토리얼에서 일단 주어진 이미지가 개인지 고양이인지 구분하는 이미지 분류기를 만들고자 한다. 그리고 추후 더 큰 규모로 확장해보자. 


Tools And Technologies


본 튜토리얼 진행을 위해서는 다음의 도구들이 필요하다. 아래는 Ubuntu 18.04 환경에서 구현되었다.

Anaconda  아나콘다는 데이터터 사이언스와 머신러닝 관련 어플리케이션 제작 관련 패키지 관리 및 배포를 도와주는 오픈 소스 프로그램이다. 설치는 다음을 참조한다.

https://www.anaconda.com/download/

Tensorflow  텐서플로우는 데이터플로우 프로그래밍을 위한 오픈 소스 소프트웨어 라이브러리이다. 설치는 다음을 참조한다. 

https://www.tensorflow.org/install/

Keras  케라스는 파이썬으로 작성된된 오픈 소스 신경망이다. 텐서플로우 환경을 활성화시킨 뒤 pip install keras 를 이용하여 케라스를 설치한다.

https://keras.io/

CNN Convolution Neural network 의 약자로 합성곱 신경망이라고 부른다. 여러 층으로 형성된 feed-forward 인공신경망으로, 대부분 시각 이미지 분석에 적용된다. 합성곱 신경망에 대한 직관적인 설명을 담은 포스팅이 아래에 있다.

https://ujjwalkarn.me/2016/08/11/intuitive-explanation-convnets/


Plan of Attack


작업 순서는 다음가 같다.

  1. Collecting the Dataset
  2. Importing Libraries and Splitting the Dataset
  3. Building the CNN
  4. Full Connection
  5. Data Augmentation
  6. Training our Network
  7. Testing


Step 1   Collecting the Dataset

머신을 훈련시키기 위해서는 어마어마한 양의 데이터를 필요로 한다. 앞으로 만들 모델이 데이터로부터 특정한 관계를 도출해내고, 해당 객체가 가진 공통적인 특징을 알아내기 위해서 말이다. 

다행스럽게도 이미 인터넷 상에는 그러한 데이터셋이 수없이 많이 올라와 있다. 여기서는 고양이와 개의 이미지 각각 5,000개, 합 10,000개로 구성된 데이터셋을 이용해보기로 하자. 

아래 링크에서 다운로드 받을 수 있다.

http://www.superdatascience.com/wp-content/uploads/2017/03/Convolutional-Neural-Networks.zip


Step 2 Importing Libraries and Splitting the Dataset

라이브러리를 가져오자. 라이브러리를 가져온 다음에는, 앞선 데이터를 훈련셋트와 검증셋트 이렇게 2개로 나눈다. 

1
2
3
4
5
6
# Importing the Keras libraries and packages
from keras.models import Sequential
from keras.layers import Convolution2D
from keras.layers import MaxPooling2D
from keras.layers import Flatten
from keras.layers import Dense
cs
Using TensorFlow backend.

앞서 다운로드받은 파일을 압축해제를 해보면, 이미 데이터셋이 2 파트로 나눠져 있음을 알 수 있다. 훈련셋트에 각가 4,000개의 고양이와 개 이미지가, 그리고 검증셋트에는 각 1,000개의 이미지가 들어가 있다.


Step 3  Buliding the CNN

가장 중요한 부분이다. 이 단계는 Convolution, Polling, Flattening 의 3부분으로 구성된다. 

Convolution 의 주요 목적은 인풋 이미지의 특성을 추출해내는 것이다. Convolution 은 입력된 데이터의 작은 사각형을 사용하여 이미지의 특징을 학습함으로써 픽셀간의 공간적 관계를 보존한다. 

모든 이미지는 픽셀값의 행렬로 간주할 수 있다. 픽셀의 값이 0과 1인 아래와 같은 5 x 5 이미지를 생각해보자. 

https://cdn-images-1.medium.com/max/800/0*JsCxaZb5AIm6YFlB

그리고 아래와 같은 3 x 3 행렬도 생각해보자.

https://cdn-images-1.medium.com/max/800/0*TG6RffpzcJBzYsEk

그러면, 5 x 5 이미지와 3 x 3 행렬의 컨벌루션은 아래 그림과 같이 계산될 수 있다.

https://cdn-images-1.medium.com/max/800/0*KdJv2eWBC1qUs3Po

결과로 획득된 행렬은 보통 특성맵이라고 불리는 것으로, 컨벌루션 진행후에는 ReLU 라는 추가적인 작업이 이루어진다. 다음 단계는 pooling 이다. 

Pooling (서브샘플링, 다운샘플링이라고 불리기도 한다) 은 각 특성맵의 차원수를 줄인다. 하지만 가장 중요한 정보는 그대로 유지한다. Max Pooling 의 경우, 공간이웃 (예를 들어, 2×2 윈도우) 을 정의하고, 그 윈도우 안에 있는 정류된 특성맵으로부터 가장 큰 원소를 취한다. 가장 큰 원소를 취하는 대신 평균값을 취하는 경우를 Average Pooling 이라고 하고, 모든 원소의 합을 취할 수도 있다. 하지만 실제로는 Max Pooling 의 성능이 가장 괜찮은 것으로 나온다. 

https://cdn-images-1.medium.com/max/800/0*6ED-178t3tjE0Wo6


Pooling 다음 단계는 flattening 이다. 신경망의 노드에 집어넣을 수 있도록 행렬을 선형 배열 변환시킨다. 

코드로 구현해보자. 

1
2
3
4
5
6
7
8
9
10
11
#Initialize the CNN
classifier = Sequential()
 
# Step1 - Convolution
classifier.add(Convolution2D(3233, input_shape = (64643), activation = 'relu'))
 
# Step 2 - Pooling
classifier.add(MaxPooling2D(pool_size = (22)))
 
# Step 3 - Flattening
classifier.add(Flatten())
cs
/home/founder/anaconda3/envs/tfKeras/lib/python3.6/site-packages/ipykernel_launcher.py:5: UserWarning: Update your `Conv2D` call to the Keras 2 API: `Conv2D(32, (3, 3), input_shape=(64, 64, 3..., activation="relu")`
  """

따라서 현재까지의 CNN 네트워크는 다음과 같은 모습이다. 

https://cdn-images-1.medium.com/max/800/1*DoRz0KaV1jwCttVDxqJflw.png


Step 4  Full Connection

Full connection 은 컨벌루션널 네트워크를 신경망과 연결한 다음 해당 네트워크를 컴파일링한다.

1
2
3
4
5
6
# Step 4 - Full Connection
classifier.add(Dense(output_dim = 128, activation = 'relu'))
classifier.add(Dense(output_dim = 1, activation = 'sigmoid'))
 
# Compling the CNN
classifier.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy'])
cs

/home/founder/anaconda3/envs/tfKeras/lib/python3.6/site-packages/ipykernel_launcher.py:2: UserWarning: Update your `Dense` call to the Keras 2 API: `Dense(activation="relu", units=128)`  

/home/founder/anaconda3/envs/tfKeras/lib/python3.6/site-packages/ipykernel_launcher.py:3: UserWarning: Update your `Dense` call to the Keras 2 API: `Dense(activation="sigmoid", units=1)

This is separate from the ipykernel package so we can avoid doing imports until

해당 오브젝트가 개인지 고양이인지에 대한 확률을 찾기 위해 마지막 층에 시그모이드 함수를 활성화함수로 하는 신경망을 만든다. 

따라서 최종 네트워크는 다음과 같은 모양을 갖추게 된다.

https://cdn-images-1.medium.com/max/800/1*aAz7Nrx4IkdEViyBknpH9Q.png


Step 5  Data Augmentation

데이터 훈련에는 많은 수의 데이터가 필요하다. 만약 제한된 수의 이미지만 네트워크상에 가지고 있다고 가정해보자. 이 경우 어떻게 해야할까?

기존의 데이터셋에 넣기 위해 새로운 이미지들을 찾아나설 필요는 없다. 왜냐요? 그 이유는 신경망은 그정도로 스마트하지는 않기 때문이다. 

예를 들어, 허접하게 훈련된 신경망의 경우 아래의 3개의 테니스공을 서로 다른 것으로 판단한다. 

https://cdn-images-1.medium.com/max/800/1*L07HTRw7zuHGT4oYEMlDig.jpeg

그러므로 더욱 많은 데이터를 얻기 위해서는 단지 기존 데이터셋에 약간의 변경만 가해도 충분하다는 것이다. 예를 들어 뒤집거나 회전시키거너 하는 등의 변화 정도 말이다. 어쨌든 우리의 신경망은 이러한 이미지들을 서로 다르게 간주한다. 

데이터 증대는 모델의 과적합을 줄이기 위한 방법으로, 우리의 훈련 데이터의 정보만을 사용하여 훈련 데이터의 양을 증가시킨다. 

사실 데이터 증대라는 영역은 새로운 것이 아니며, 다수의 데이터 증대 테크닉이 특정 문제 해결에 적용되어 왔다. 

코드를 보자. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Part 2 - Fitting the CNN to the images
from keras.preprocessing.image import ImageDataGenerator
 
train_datagen = ImageDataGenerator(
        rescale = 1./255,
        shear_range = 0.2,
        zoom_range = 0.2,
        horizontal_flip = True)
 
test_datagen = ImageDataGenerator(rescale = 1./255)
 
training_set = train_datagen.flow_from_directory(
        '../input/Convolutional_Neural_Networks/dataset/training_set',
        target_size = (64,64),
        batch_size = 32,
        class_mode = 'binary')
 
test_set = test_datagen.flow_from_directory(
        '../input/Convolutional_Neural_Networks/dataset/test_set',
        target_size =(6464),
        batch_size = 32,
        class_mode = 'binary')Colored by Color Scripter
cs
Found 8000 images belonging to 2 classes.
Found 2000 images belonging to 2 classes.

이제 많은 데이터를 가지게 되었다. 트레이닝을 시작해보자.


Step 6  Training our Network

이로써 구축에 필요한 모든 단계를 마쳤다. 이제 모델을 훈련시킬 타이밍이다. 훈련에 앞서 훈련과정을 텐서보드를 통해 확인해보자. 다음과 같이 구성한다.

1
2
3
4
# Early stopping callback
from keras.callbacks import EarlyStopping, TensorBoard
PATIENCE = 10
early_stopping = EarlyStopping(monitor='loss', min_delta=0, patience=PATIENCE, verbose=0, mode='auto')
cs

로그가 저장될 디렉토리를 생성한 후 다음과 같이 로그 루트 디렉토리에 해당 경로를 넣는다.

1
2
3
4
5
6
# TensorBoard callback
from datetime import datetime
LOG_DIRECTORY_ROOT = '/home/founder/tfKeras/tensorlog'
now = datetime.utcnow().strftime("%Y%m%d%H%M%S")
log_dir = "{}/run-{}/".format(LOG_DIRECTORY_ROOT, now)
tensorboard = TensorBoard(log_dir=log_dir, write_graph=True, write_images=True)
cs

1
2
# Place the callbacks in a list
callbacks = [early_stopping, tensorboard]
cs

먼저 에포크를 10으로 설정해서 진행한다.

1
2
3
4
5
6
7
8
9
10
from IPython.display import display
from PIL import Image
 
classifier.fit_generator(
        training_set,
        steps_per_epoch = 8000,
        epochs = 10,
        validation_data = test_set,
        validation_steps = 800,
        callbacks = callbacks)
cs

이 단계에서 ModuleNotFoundError: No module named 'PIL' 과 UnsatisfiableError 를 발생시키는 경우 다음 링크를 참고한다. 

ModuleNotFoundError: No module named 'PIL' https://antilibrary.org/1976


텐서보드를 실행하자.

1
(tfKeras) founder@hilbert:~/tfKeras$ tensorboard --logdir /home/founder/tfKeras/tensorlog
cs


훈련과정에서 GPU 환경여부에 따라 실행속도에 큰 차이를 나는 것을 확인할 수 있다. 본 튜토리얼은 구글 클라우드 플랫폼의 VM 에서 구동하고 있다.

n1-standard-2(vCPU 1개, 3.75GB 메모리) 의 경우 CPU가 100% 를 찍고 있다.


이에 n1-standard-2(vCPU 2개, 7.5GB 메모리) 으로 변경, 80~80% 대로 떨어졌다.


위 환경에서 소요된 시간을 보면, 에포크 당 약 50여분이 소요되었다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Epoch 1/10
8000/8000 [==============================- 3144s 393ms/step - loss: 0.5088 - acc: 0.7426 - val_loss: 0.5119 - val_acc: 0.7744
Epoch 2/10
8000/8000 [==============================- 3153s 394ms/step - loss: 0.3907 - acc: 0.8200 - val_loss: 0.5637 - val_acc: 0.7650
Epoch 3/10
8000/8000 [==============================- 3126s 391ms/step - loss: 0.3391 - acc: 0.8464 - val_loss: 0.6233 - val_acc: 0.7619
Epoch 4/10
8000/8000 [==============================- 3108s 388ms/step - loss: 0.2962 - acc: 0.8681 - val_loss: 0.7462 - val_acc: 0.7465
Epoch 5/10
8000/8000 [==============================- 3094s 387ms/step - loss: 0.2576 - acc: 0.8894 - val_loss: 0.8374 - val_acc: 0.7563
Epoch 6/10
8000/8000 [==============================- 3080s 385ms/step - loss: 0.2246 - acc: 0.9057 - val_loss: 1.0228 - val_acc: 0.7480
Epoch 7/10
8000/8000 [==============================- 3140s 392ms/step - loss: 0.1961 - acc: 0.9187 - val_loss: 1.1654 - val_acc: 0.7290
Epoch 8/10
8000/8000 [==============================- 3160s 395ms/step - loss: 0.1720 - acc: 0.9300 - val_loss: 1.1737 - val_acc: 0.7429
Epoch 9/10
8000/8000 [==============================- 3065s 383ms/step - loss: 0.1526 - acc: 0.9385 - val_loss: 1.0695 - val_acc: 0.7506
Epoch 10/10
8000/8000 [==============================- 3027s 378ms/step - loss: 0.1386 - acc: 0.9450 - val_loss: 1.3196 - val_acc: 0.7425
<keras.callbacks.History at 0x7efbf67989e8>

이번에는 에포크를 20으로 설정해서 진행하였다. 그리고 에포크 수가 증가할수록, 정확도 역시 향상됨을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Epoch 1/20
8000/8000 [==============================- 2926s 366ms/step - loss: 0.4128 - acc: 0.8060 - val_loss: 0.5941 - val_acc: 0.7483
Epoch 2/20
8000/8000 [==============================- 2935s 367ms/step - loss: 0.3268 - acc: 0.8540 - val_loss: 0.7432 - val_acc: 0.7540
Epoch 3/20
8000/8000 [==============================- 2934s 367ms/step - loss: 0.2607 - acc: 0.8873 - val_loss: 0.8729 - val_acc: 0.7516
Epoch 4/20
8000/8000 [==============================- 2932s 366ms/step - loss: 0.2155 - acc: 0.9094 - val_loss: 1.0187 - val_acc: 0.7384
Epoch 5/20
8000/8000 [==============================- 2927s 366ms/step - loss: 0.1759 - acc: 0.9282 - val_loss: 1.2049 - val_acc: 0.7494
Epoch 6/20
8000/8000 [==============================- 2922s 365ms/step - loss: 0.1451 - acc: 0.9420 - val_loss: 1.3610 - val_acc: 0.7400
Epoch 7/20
8000/8000 [==============================- 2918s 365ms/step - loss: 0.1249 - acc: 0.9511 - val_loss: 1.2979 - val_acc: 0.7379
Epoch 8/20
8000/8000 [==============================- 2923s 365ms/step - loss: 0.1087 - acc: 0.9578 - val_loss: 1.5250 - val_acc: 0.7383
Epoch 9/20
8000/8000 [==============================- 2922s 365ms/step - loss: 0.0970 - acc: 0.9632 - val_loss: 1.7719 - val_acc: 0.7266
Epoch 10/20
8000/8000 [==============================- 2917s 365ms/step - loss: 0.0853 - acc: 0.9680 - val_loss: 1.7015 - val_acc: 0.7451
Epoch 11/20
8000/8000 [==============================- 2912s 364ms/step - loss: 0.0771 - acc: 0.9719 - val_loss: 1.6849 - val_acc: 0.7402
Epoch 12/20
8000/8000 [==============================- 2910s 364ms/step - loss: 0.0687 - acc: 0.9750 - val_loss: 1.7218 - val_acc: 0.7428
Epoch 13/20
8000/8000 [==============================- 2906s 363ms/step - loss: 0.0620 - acc: 0.9776 - val_loss: 1.8727 - val_acc: 0.7567
Epoch 14/20
8000/8000 [==============================- 2902s 363ms/step - loss: 0.0567 - acc: 0.9798 - val_loss: 2.1399 - val_acc: 0.7255
Epoch 15/20
8000/8000 [==============================- 2911s 364ms/step - loss: 0.0517 - acc: 0.9821 - val_loss: 1.9402 - val_acc: 0.7369
Epoch 16/20
8000/8000 [==============================- 2914s 364ms/step - loss: 0.0477 - acc: 0.9833 - val_loss: 2.0261 - val_acc: 0.7390
Epoch 17/20
8000/8000 [==============================- 2920s 365ms/step - loss: 0.0456 - acc: 0.9847 - val_loss: 2.1458 - val_acc: 0.7324
Epoch 18/20
8000/8000 [==============================- 2917s 365ms/step - loss: 0.0429 - acc: 0.9852 - val_loss: 2.1210 - val_acc: 0.7402
Epoch 19/20
8000/8000 [==============================- 2920s 365ms/step - loss: 0.0400 - acc: 0.9868 - val_loss: 2.1081 - val_acc: 0.7333
Epoch 20/20
8000/8000 [==============================- 2924s 366ms/step - loss: 0.0373 - acc: 0.9874 - val_loss: 2.0623 - val_acc: 0.7377
<keras.callbacks.History at 0x7f8716a21668>
cs


위의 훈련과정을 텐서보드를 통해 확인해보자.

웹브라우저에서 http://your_ip:6006 로 접속한다.





이제 파일 형태로 저장해보자. 모델을 json 파일로, 그리고 가중치를 h5 파일로 저장한다. 다음과 같다.

1
2
3
classifier_json = classifier.to_json()
with open("classifier.json""w") as json_file : 
    json_file.write(classifier_json)
cs


1
2
classifier.save_weights("model.h5")
print("Saved classifier to disk")
cs

Saved classifier to disk



Step 7   Testing

이제 이미지를 랜덤하게 선택하여 테스트해보자.


https://cdn-images-1.medium.com/max/800/1*Fm-xFCVAjnB4WPH5NV0eRQ.png


위의 파일을 random.jpg 로 저장하고 테스트해봤다. '개'라고 정확히 출력결과를 보여준다. 물론 대부분의 경우 정확한 예측을 내놓겠지만, 항상 그런 것은 아니다. 위 이미지를 포함하여 검증세트에 없는 15개 정도의 이미지로 테스트를 해봤는데 3개 정도가 잘못된 예측을 내놓았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
from keras.preprocessing import image
test_image = image.load_img('random.jpg', target_size = (6464))
test_image = image.img_to_array(test_image)
test_image = np.expand_dims(test_image, axis = 0)
result = classifier.predict(test_image)
training_set.class_indices
 
if result[0][0>= 0.5:
    prediction = '개'
else:
    prediction = '고양이'
print(prediction)
cs


추가적인 합성곱과 풀링 레이어를 추가하고, 노드와 에포크수를 증가시키면 좀 더 정확한 결과를 얻을 수 있을 것이다. 

이상으로 아주 간단한 이미지 인식 분류기를 만들어 보았다. 위의 컨셉은 충분한 훈련데이터와 적절한 네트워크가 있다면 다방면으로 활용이 가능하다. 

“Be quiet, darling. Let pattern recognition have its way.” 

― William Gibson, The Peripheral


튜토리얼 원문출처

https://medium.com/nybles/create-your-first-image-recognition-classifier-using-cnn-keras-and-tensorflow-backend-6eaab98d14dd