블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

 

얼굴 인식 모델을 만들어보자

#3 - 학습된 모델로 예측하기


조대협 (http://bcho.tistory.com)


앞글에 걸쳐서 얼굴 인식을 위한 데이타를 수집 및 정재하고, 이를 기반으로 얼굴 인식 모델을 학습 시켰다.

 

 

이번글에서는 학습이 된 데이타를 가지고, 사진을 넣어서 실제로 인식하는 코드를 만들어보자

전체 소스 코드는 https://github.com/bwcho75/facerecognition/blob/master/2.%2BFace%2BRecognition%2BPrediction%2BTest.ipynb 와 같다.

모델 로딩 하기

 

모델 학습에 사용한 CNN 모델을 똑같이 정의한다. conv1(),conv2(),conv3(),conv4(),fc1(),fc2(), build_model() 등 학습에 사용된 CNN 네트워크를 똑같이 정의하면 된다.

 

다음으로 이 모델에 학습된 값들을 채워 넣어야 한다.

# build graph

images = tf.placeholder(tf.float32,[None,FLAGS.image_size,FLAGS.image_size,FLAGS.image_color])

keep_prob = tf.placeholder(tf.float32) # dropout ratio

 

예측에 사용할 image 를 넘길 인자를  images라는 플레이스홀더로 정의하고, dropout 비율을 정하는 keep_prob도 플레이스 홀더로 정의한다.

 

prediction = tf.nn.softmax(build_model(images,keep_prob))

 

그래프를 만드는데, build_model에 의해서 나온 예측 결과에 softmax 함수를 적용한다. 학습시에는 softmax 함수의 비용이 크기 때문에 적용하지 않았지만, 예측에서는 결과를 쉽게 알아보기 위해서  softmax 함수를 적용한다. Softmax 함수는 카테고리 별로 확률을 보여줄때 전체 값을 1.0으로 해서 보여주는것인데, 만약에 Jolie,Sulyun,Victora 3개의 카테코리가 있을때 각각의 확률이 70%,20%,10%이면 Softmax를 적용한 결과는 [0.7,0.2,0.1] 식으로 출력해준다.

 

sess = tf.InteractiveSession()

sess.run(tf.global_variables_initializer())

 

다음 텐서플로우 세션을 초기화 하고,

 

saver = tf.train.Saver()

saver.restore(sess, 'face_recog')

 

마지막으로 Saver의 restore 함수를 이용하여 ‘face_recog’라는 이름으로 저장된 학습 결과를 리스토어 한다. (앞의 예제에서, 학습이 완료된 모델을 ‘face_recog’라는 이름으로 저장하였다.)

 

예측하기

로딩 된 모델을 가지고 예측을 하는 방법은 다음과 같다. 이미지 파일을 읽은 후에, 구글 클라우드 VISION API를 이용하여, 얼굴의 위치를 추출한후, 얼굴 이미지만 크롭핑을 한후에, 크롭된 이미지를 텐서플로우 데이타형으로 바꾼후에, 앞서 로딩한 모델에 입력하여 예측된 결과를 받게 된다.

 

얼굴 영역 추출하기

먼저 vision API로 얼굴 영역을 추출하는 부분이다. 앞의 이미지 전처리에 사용된 부분과 다르지 않다.

 

import google.auth

import io

import os

from oauth2client.client import GoogleCredentials

from google.cloud import vision

from PIL import Image

from PIL import ImageDraw

 

FLAGS.image_size = 96

 

# set service account file into OS environment value

os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/Users/terrycho/keys/terrycho-ml.json"

 

위와 같이 구글 클라우드 Vision API를 사용하기 위해서 억세스 토큰을 Service Account 파일로 다운 받아서 위와 같이 GOOGLE_APPLICATION_CREDENTIALS 환경 변수에 세팅 하였다.

 

visionClient = vision.Client()

print ('[INFO] processing %s'%(imagefile))

 

#detect face

image = visionClient.image(filename=imagefile)

faces = image.detect_faces()

face = faces[0]

 

다음 vision API 클라이언트를 생성한 후에, detect_faces() 를 이용하여 얼굴 정보를 추출해낸다.

 

print 'number of faces ',len(faces)

 

#get face location in the photo

left = face.fd_bounds.vertices[0].x_coordinate

top = face.fd_bounds.vertices[0].y_coordinate

right = face.fd_bounds.vertices[2].x_coordinate

bottom = face.fd_bounds.vertices[2].y_coordinate

rect = [left,top,right,bottom]

 

추출된 얼굴 정보에서 첫번째 얼굴의 위치 (상하좌우) 좌표를 리턴 받는다.

얼굴 영역을 크롭하기

앞에서 입력 받은 상하좌우 좌표를 이용하여, 이미지 파일을 열고,  크롭한다.

 

fd = io.open(imagefile,'rb')

image = Image.open(fd)

 

import matplotlib.pyplot as plt

# display original image

print "Original image"

plt.imshow(image)

plt.show()

 

 

# draw green box for face in the original image

print "Detect face boundary box "

draw = ImageDraw.Draw(image)

draw.rectangle(rect,fill=None,outline="green")

 

plt.imshow(image)

plt.show()

 

crop = image.crop(rect)

im = crop.resize((FLAGS.image_size,FLAGS.image_size),Image.ANTIALIAS)

plt.show()

im.save('cropped'+imagefile)

 

크롭된 이미지를 텐서플로우에서 읽는다.

 

print "Cropped image"

tfimage = tf.image.decode_jpeg(tf.read_file('cropped'+imagefile),channels=3)

tfimage_value = tfimage.eval()

 

크롭된 파일을 decode_jpeg() 메서드로 읽은 후에, 값을 tfimage.eval()로 읽어드린다.

 

tfimages = []

tfimages.append(tfimage_value)

 

앞에서 정의된 모델이 한개의 이미지를 인식하는게 아니라 여러개의 이미지 파일을 동시에 읽도록 되어 있기 때문에, tfimages라는 리스트를 만든 후, 인식할 이미지를 붙여서 전달한다.

 

plt.imshow(tfimage_value)

plt.show()

fd.close()

 

p_val = sess.run(prediction,feed_dict={images:tfimages,keep_prob:1.0})

name_labels = ['Jessica Alba','Angelina Jolie','Nicole Kidman','Sulhyun','Victoria Beckam']

i = 0

for p in p_val[0]:

   print('%s %f'% (name_labels[i],float(p)) )

   i = i + 1

 

tfimages 에 이미지를 넣어서 모델에 넣고 prediction 값을 리턴 받는다. dropout은 사용하지 않기 때문에, keep_prob을 1.0으로 한다.

나온 결과를 가지고 Jessica, Jolie,Nicole Kidman, Sulhyun, Victoria Beckam 일 확률을 각각 출력한다.


전체 코드는 https://github.com/bwcho75/facerecognition/blob/master/2.%2BFace%2BRecognition%2BPrediction%2BTest.ipynb


다음은 설현 사진을 가지고 예측을 한 결과 이다.


 

이 코드는 학습된 모델을 기반으로 얼굴을 인식이 가능하기는 하지만 실제 운영 환경에 적용하기에는 부족하다. 파이썬 모델 코드를 그대로 옮겼기 때문에, 성능도 상대적으로 떨어지고, 실제 운영에서는 모델을 업그레이드 배포 할 수 있고, 여러 서버를 이용하여 스케일링도 지원해야 한다.

그래서 텐서플로우에서는 Tensorflow Serving 이라는 예측 서비스 엔진을 제공하고 구글 클라우에서는 Tensorflow Serving의 매니지드 서비스인, CloudML 서비스를 제공한다.

 

앞의 두 글이 로컬 환경에서 학습과 예측을 진행했는데, 다음 글에서는 상용 서비스에 올릴 수 있는 수준으로 학습과 예측을 할 수 있는 방법에 대해서 알아보도록 하겠다.

 

저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

연예인 얼굴 인식 모델을 만들어보자

#2 CNN 모델을 만들고 학습 시켜보기

조대협 (http://bcho.tistroy.com)

선행 학습 자료

이 글은 딥러닝 컨볼루셔널 네트워크 (이하 CNN)을 이용하여 사람의 얼굴을 인식하는 모델을 만드는 튜토리얼이다. 이 글을 이해하기 위해서는 머신러닝과 컨볼루셔널 네트워크등에 대한 사전 지식이 필요한데, 사전 지식이 부족한 사람은 아래 글을 먼저 읽어보기를 추천한다.

 

머신러닝의 개요 http://bcho.tistory.com/1140

머신러닝의 기본 원리는 http://bcho.tistory.com/1139

이산 분류의 원리에 대해서는 http://bcho.tistory.com/1142

인공 신경망에 대한 개념은 http://bcho.tistory.com/1147

컨볼루셔널 네트워크에 대한 개념 http://bcho.tistory.com/1149

학습용 데이타 전처리 http://bcho.tistory.com/1176

학습용 데이타 전처리를 스케일링 하기 http://bcho.tistory.com/1177

손글씨를 CNN을 이용하여 인식하는 모델 만들기 http://bcho.tistory.com/1156

손글씨 인식 CNN 모델을 이용하여 숫자 인식 하기 http://bcho.tistory.com/1157

환경

본 예제는 텐서플로우 1.1과 파이썬 2.7 그리고 Jupyter 노트북 환경 및 구글 클라우드를 사용하여 개발되었다.

준비된 데이타

학습에 사용한 데이타는 96x96 사이즈의 얼굴 이미지로, 총 5명의 사진(안젤리나 졸리, 니콜키드만, 제시카 알바, 빅토리아 베컴,설현)을 이용하였으며, 인당 학습 데이타 40장 테스트 데이타 10장으로 총 250장의 얼굴 이미지를 사용하였다.

사전 데이타를 준비할때, 정면 얼굴을 사용하였으며, 얼굴 각도 변화 폭이 최대한 적은 이미지를 사용하였다. (참고 : https://www.slideshare.net/Byungwook/ss-76098082 ) 만약에 이 모델로 학습이 제대로 되지 않는다면 학습에 사용된 데이타가 적절하지 않은것이기 때문에 데이타를 정재해서 학습하기를 권장한다.

데이타 수집 및 정재 과정에 대한 내용은 http://bcho.tistory.com/1177 를 참고하기 바란다.

 

컨볼루셔널 네트워크 모델

얼굴 인식을 위해서, 머신러닝 모델 중 이미지 인식에 탁월한 성능을 보이는 CNN 모델을 사용하였다. 테스트용 모델이기 때문에 모델은 복잡하지 않게 설계하였다.

 

학습과 예측에 사용되는 이미지는 96x96픽셀의 RGB 컬러 이미지를 사용하였다.

아래 그림과 같은 모델을 사용했는데, 총 4개의 Convolutional 계층과, 2개의 Fully connected 계층, 하나의 Dropout 계층을 사용하였다.


Convolutional 계층의 크기는 각각 16,32,64,128개를 사용하였고, 사용된 Convolutional 필터의 사이즈는 3x3 이다.

Fully connected 계층은 각각 512, 1024를 사용하였고 Dropout 계층에서는 Keep_prob값을 0.7로 둬서 30%의 뉴론이 drop out 되도록 하여 학습을 진행하였다.

 

학습 결과 5개의 카테고리에 대해서 총 200장의 이미지로 맥북 프로 i7 CPU 기준 7000 스텝정도의 학습을 진행한 결과 테스트 정확도 기준 90% 정도의 정확도를 얻을 수 있었다.

코드 설명

텐서플로우로 구현된 코드를 살펴보자

파일에서 데이타 읽기

먼저 학습 데이타를 읽어오는 부분이다.

학습과 테스트에서 읽어드리는 데이타의 포맷은 다음과 같다

 

/Users/terrycho/training_data_class5_40/validate/s1.jpg,Sulhyun,3

이미지 파일 경로, 사람 이름 , 숫자 라벨

 

파일에서 데이타를 읽어서 처리 하는 함수는 read_data_batch(), read_data(), get_input_queue()  세가지 함수가 사용된다.

  • get_input_queue() 함수는 CSV 파일을 한줄씩 읽어서, 파일 경로 및 숫자 라벨 두가지를 리턴할 수 있는 큐를 만들어서 리턴한다.

  • read_data() 함수는 get_input_queue()에서 리턴한 큐로 부터 데이타를 하나씩 읽어서 리턴한다.

  • read_batch_data()함수는 read_data() 함수를 이용하여, 데이타를 읽어서 일정 단위(배치)로 묶어서 리턴을 하고, 그 과정에서 이미지 데이타를 뻥튀기 하는 작업을 한다.

즉 호출 구조는 다음과 같다.

 

read_batch_data():

 → Queue = get_input_queue()

 → image,label = read_data(Queue)

 → image_data = 이미지 데이타 뻥튀기

Return image_data,label

 

실제 코드를 보자

get_input_queue

get_input_queue() 함수는 CSV 파일을 읽어서 image와 labels을 리턴하는 input queue를 만들어서 리턴하는 함수이다.

 

def get_input_queue(csv_file_name,num_epochs = None):

   train_images = []

   train_labels = []

   for line in open(csv_file_name,'r'):

       cols = re.split(',|\n',line)

       train_images.append(cols[0])

       # 3rd column is label and needs to be converted to int type

       train_labels.append(int(cols[2]) )

                           

   input_queue = tf.train.slice_input_producer([train_images,train_labels],

                                              num_epochs = num_epochs,shuffle = True)

   

   return input_queue

 

CSV 파일을 순차적으로 읽은 후에, train_images와 train_labels라는 배열에 넣은 다음 tf.train.slice_input_producer를 이용하여 큐를 만들어냈다. 이때 중요한 점은 shuffle=True라는 옵션을 준것인데, 만약에 이 옵션을 주지 않으면, 학습 데이타를 큐에서 읽을때 CSV에서 읽은 순차적으로 데이타를 리턴한다. 즉 현재 데이타 포맷은 Jessica Alba가 40개, Jolie 가 40개, Nicole Kidman이 40개 .. 식으로 순서대로 들어가 있기 때문에, Jessica Alba를 40개 리턴한 후 Jolie를 40개 리턴하는 식이 된다.  이럴 경우 Convolutional 네트워크가 Jessica Alba에 치우쳐지기 때문에 제대로 학습이 되지 않는다. Shuffle은 필수이다.

read_data()

input_queue에서 데이타를 읽는 부분인데 특이한 점은 input_queue에서 읽어드린 이미지 파일명의 파일을 읽어서 데이타 객체로 저장해야 한다. 텐서플로우에서는 tf.image.decode_jpeg, tf.image.decode_png 등을 이용하여 이러한 기능을 제공한다.

def read_data(input_queue):

   image_file = input_queue[0]

   label = input_queue[1]

   

   image =  tf.image.decode_jpeg(tf.read_file(image_file),channels=FLAGS.image_color)

   

   return image,label,image_file

read_data_batch()

마지막으로 read_data_batch() 함수 부분이다.get_input_queue에서 읽은 큐를 가지고 read_data함수에 넣어서 이미지 데이타와 라벨을 읽어서 리턴하는 값을 받아서 일정 단위로 (배치) 묶어서 리턴하는 함수이다. 중요한 부분이 데이타를 뻥튀기 하는 부분이 있다.

이 모델에서 학습 데이타가 클래스당 40개 밖에 되지 않기 때문에 학습데이타가 부족하다. 그래서 여기서 사용한 방법은 read_data에서 리턴된 이미지 데이타에 대해서 tf.image.random_xx 함수를 이용하여 좌우를 바꾸거나, brightness,contrast,hue,saturation 함수를 이용하여 매번 색을 바꿔서 리턴하도록 하였다.

 

def read_data_batch(csv_file_name,batch_size=FLAGS.batch_size):

   input_queue = get_input_queue(csv_file_name)

   image,label,file_name= read_data(input_queue)

   image = tf.reshape(image,[FLAGS.image_size,FLAGS.image_size,FLAGS.image_color])

   

   # random image

   image = tf.image.random_flip_left_right(image)

   image = tf.image.random_brightness(image,max_delta=0.5)

   image = tf.image.random_contrast(image,lower=0.2,upper=2.0)

   image = tf.image.random_hue(image,max_delta=0.08)

   image = tf.image.random_saturation(image,lower=0.2,upper=2.0)

   

   batch_image,batch_label,batch_file = tf.train.batch([image,label,file_name],batch_size=batch_size)

   #,enqueue_many=True)

   batch_file = tf.reshape(batch_file,[batch_size,1])

 

   batch_label_on_hot=tf.one_hot(tf.to_int64(batch_label),

       FLAGS.num_classes, on_value=1.0, off_value=0.0)

   return batch_image,batch_label_on_hot,batch_file

 

그리고 마지막 부분에 label을 tf.one_hot을 이용해서 변환한것을 볼 수 있는데, 입력된 label은 0,1,2,3,4 과 같은 단일 정수이다. 그런데, CNN에서 나오는 결과는 정수가 아니라 클래스가 5개인 (분류하는 사람이 5명이기 때문에) 행렬이다. 즉 Jessica Alba일 가능성이 90%이고, Jolie일 가능성이 10%이면 결과는 [0.9,0.1,0,0,0] 식으로 리턴이 되기 때문에, 입력된 라벨 0은 [1,0,0,0,0], 라벨 1은 [0,1,0,0,0] 라벨 2는 [0,0,1,0,0] 식으로 변환되어야 한다. tf.one_hot 이라는 함수가 이 기능을 수행해준다.

 

모델 코드

모델은 앞서 설명했듯이 4개의 Convolutional 계층과, 2개의 Fully connected 계층 그리고 Dropout 계층을 사용한다. 각각의 계층별로는 코드가 다르지 않고 인지만 다르니 하나씩 만 설명하도록 한다.

 

Convolutional 계층

아래 코드는 두번째 Convolutional 계층의 코드이다.

  • FLAGS.conv2_layer_size 는 이 Convolutional 계층의 뉴런의 수로 32개를 사용한다.

  • FLAGS.conv2_filter_size 는 필터 사이즈를 지정하는데, 3x3 을 사용한다.

  • FLAGS.stride2 = 1 는 필터의 이동 속도로 한칸씩 이동하도록 정의했다.

 

# convolutional network layer 2

def conv2(input_data):

   FLAGS.conv2_filter_size = 3

   FLAGS.conv2_layer_size = 32

   FLAGS.stride2 = 1

   

   with tf.name_scope('conv_2'):

       W_conv2 = tf.Variable(tf.truncated_normal(

                       [FLAGS.conv2_filter_size,FLAGS.conv2_filter_size,FLAGS.conv1_layer_size,FLAGS.conv2_layer_size],

                                             stddev=0.1))

       b2 = tf.Variable(tf.truncated_normal(

                       [FLAGS.conv2_layer_size],stddev=0.1))

       h_conv2 = tf.nn.conv2d(input_data,W_conv2,strides=[1,1,1,1],padding='SAME')

       h_conv2_relu = tf.nn.relu(tf.add(h_conv2,b2))

       h_conv2_maxpool = tf.nn.max_pool(h_conv2_relu

                                       ,ksize=[1,2,2,1]

                                       ,strides=[1,2,2,1],padding='SAME')

       

       

   return h_conv2_maxpool

 

다음 Weight 값 W_conv2 와 Bias 값 b2를 지정한후에, 간단하게 tf.nn.conv2d 함수를 이용하면 2차원의 Convolutional 네트워크를 정의해준다. 다음 결과가 나오면 이 결과를 액티베이션 함수인 relu 함수에 넣은 후에, 마지막으로 max pooling 을 이용하여 결과를 뽑아낸다.

 

각 값의 의미에 대해서는 http://bcho.tistory.com/1149 의 컨볼루셔널 네트워크 개념 글을 참고하기 바란다.

같은 방법으로 총 4개의 Convolutional 계층을 중첩한다.

 

Fully Connected 계층

앞서 정의한 4개의 Convolutional 계층을 통과하면 다음 두개의 Fully Connected 계층을 통과하게 되는데 모양은 다음과 같다.

  • FLAGS.fc1_layer_size = 512 를 통하여 Fully connected 계층의 뉴런 수를 512개로 지정하였다.

 

# fully connected layer 1

def fc1(input_data):

   input_layer_size = 6*6*FLAGS.conv4_layer_size

   FLAGS.fc1_layer_size = 512

   

   with tf.name_scope('fc_1'):

       # 앞에서 입력받은 다차원 텐서를 fcc에 넣기 위해서 1차원으로 피는 작업

       input_data_reshape = tf.reshape(input_data, [-1, input_layer_size])

       W_fc1 = tf.Variable(tf.truncated_normal([input_layer_size,FLAGS.fc1_layer_size],stddev=0.1))

       b_fc1 = tf.Variable(tf.truncated_normal(

                       [FLAGS.fc1_layer_size],stddev=0.1))

       h_fc1 = tf.add(tf.matmul(input_data_reshape,W_fc1) , b_fc1) # h_fc1 = input_data*W_fc1 + b_fc1

       h_fc1_relu = tf.nn.relu(h_fc1)

   

   return h_fc1_relu

 

Fully connected 계층은 단순하게 relu(W*x + b) 함수이기 때문에 이 함수를 위와 같이 그대로 적용하였다.

마지막 계층

Fully connected 계층을 거쳐 나온 데이타는 Dropout 계층을 거친후에, 5개의 카테고리에 대한 확률로 결과를 내기 위해서 final_out 계층을 거치게 되는데, 이 과정에서 softmax 함수를 사용해야 하나, 학습 과정에서는 별도로 softmax 함수를 사용하지 않는다. softmax는 나온 결과의 합이 1.0이 되도록 값을 변환해주는 것인데, 학습 과정에서는 5개의 결과 값이 어떤 값이 나오던 가장 큰 값에 해당하는 것이 예측된 값이기 때문에, 그 값과 입력된 라벨을 비교하면 되기 때문이다.

즉 예를 들어 Jessica Alba일 확률이 100%면 실제 예측에서는 [1,0,0,0,0] 식으로 결과가 나와야 되지만, 학습 중는 Jessica Alaba 로 예측이 되었다고만 알면 되기 때문에 결과가 [1292,-0.221,-0.221,-0.221] 식으로 나오더라도 최대값만 찾으면 되기 때문에 별도로 softmax 함수를 적용할 필요가 없다. Softmax 함수는 연산 비용이 큰 함수이기 때문에 일반적으로 학습 단계에서는 적용하지 않는다.

 

마지막 계층의 코드는 다음과 같다.

# final layer

def final_out(input_data):

 

   with tf.name_scope('final_out'):

       W_fo = tf.Variable(tf.truncated_normal([FLAGS.fc2_layer_size,FLAGS.num_classes],stddev=0.1))

       b_fo = tf.Variable(tf.truncated_normal(

                       [FLAGS.num_classes],stddev=0.1))

       h_fo = tf.add(tf.matmul(input_data,W_fo) , b_fo) # h_fc1 = input_data*W_fc1 + b_fc1

       

   # 최종 레이어에 softmax 함수는 적용하지 않았다.

       

   return h_fo

전체 네트워크 모델 정의

이제 각 CNN의 각 계층을 함수로 정의 하였으면 각 계층을 묶어 보도록 하자. 묶는 법은 간단하다 앞 계층에서 나온 계층을 순서대로 배열하고 앞에서 나온 결과를 뒤의 계층에 넣는 식으로 묶으면 된다.

 

# build cnn_graph

def build_model(images,keep_prob):

   # define CNN network graph

   # output shape will be (*,48,48,16)

   r_cnn1 = conv1(images) # convolutional layer 1

   print ("shape after cnn1 ",r_cnn1.get_shape())

   

   # output shape will be (*,24,24,32)

   r_cnn2 = conv2(r_cnn1) # convolutional layer 2

   print ("shape after cnn2 :",r_cnn2.get_shape() )

   

   # output shape will be (*,12,12,64)

   r_cnn3 = conv3(r_cnn2) # convolutional layer 3

   print ("shape after cnn3 :",r_cnn3.get_shape() )

 

   # output shape will be (*,6,6,128)

   r_cnn4 = conv4(r_cnn3) # convolutional layer 4

   print ("shape after cnn4 :",r_cnn4.get_shape() )

   

   # fully connected layer 1

   r_fc1 = fc1(r_cnn4)

   print ("shape after fc1 :",r_fc1.get_shape() )

 

   # fully connected layer2

   r_fc2 = fc2(r_fc1)

   print ("shape after fc2 :",r_fc2.get_shape() )

   

   ## drop out

   # 참고 http://stackoverflow.com/questions/34597316/why-input-is-scaled-in-tf-nn-dropout-in-tensorflow

   # 트레이닝시에는 keep_prob < 1.0 , Test 시에는 1.0으로 한다.

   r_dropout = tf.nn.dropout(r_fc2,keep_prob)

   print ("shape after dropout :",r_dropout.get_shape() )

   

   # final layer

   r_out = final_out(r_dropout)

   print ("shape after final layer :",r_out.get_shape() )

 

   return r_out

 

이 build_model 함수는 image 를 입력 값으로 받아서 어떤 카테고리에 속할지를 리턴하는 컨볼루셔널 네트워크이다.  중간에 Dropout 계층이 추가되어 있는데, tf.nn.dropout함수를 이용하면 간단하게 dropout 계층을 구현할 수 있다. r_fc2는 Dropout 계층 앞의 Fully Connected 계층에서 나온 값이고,  두번째 인자로 남긴 keep_prob는 Dropout 비율이다.

 

   r_dropout = tf.nn.dropout(r_fc2,keep_prob)

   print ("shape after dropout :",r_dropout.get_shape() )

 

모델 학습

데이타를 읽는 부분과 학습용 모델 정의가 끝났으면 실제로 학습을 시켜보자

 

def main(argv=None):

   

   # define placeholders for image data & label for traning dataset

   

   images = tf.placeholder(tf.float32,[None,FLAGS.image_size,FLAGS.image_size,FLAGS.image_color])

   labels = tf.placeholder(tf.int32,[None,FLAGS.num_classes])

   image_batch,label_batch,file_batch = read_data_batch(TRAINING_FILE)

 

먼저 학습용 모델에 넣기 위한 image 데이타를 읽어드릴 placeholder를 images로 정의하고, 다음으로 모델에 의해 계산된 결과와 비교하기 위해서 학습데이타에서 읽어드린 label 데이타를 저장하기 위한 placeholder를 labels로 정의한다. 다음 image_batch,label_batch,fle_batch 변수에 배치로 학습용 데이타를 읽어드린다. 그리고 dropout 계층에서 dropout 비율을 지정할 keep_prob를 place holder로 정의한다.

각 변수가 지정되었으면, build_model 함수를 호출하여, images 값과 keep_prob 값을 넘겨서 Convolutional 네트워크에 값을 넣도록 그래프를 정의하고 그 결과 값을 prediction으로 정의한다.

 

   keep_prob = tf.placeholder(tf.float32) # dropout ratio

   prediction = build_model(images,keep_prob)

   # define loss function

   loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=prediction,labels=labels))

   tf.summary.scalar('loss',loss)

 

   #define optimizer

   optimizer = tf.train.AdamOptimizer(FLAGS.learning_rate)

   train = optimizer.minimize(loss)

 

중간 중간에 학습 과정을 시각화 하기 위해서 tf.summary.scalar 함수를 이용하여 loss 값을 저장하였다.

 

그래프 생성이 완료 되었으면, 학습에서 계산할 비용 함수를 정의한다. 비용함수는 sofrmax cross entopy 함수를 이용하여, 모델에 의해서 예측된 값 prediction 과, 학습 파일에서 읽어드린 label 값을 비교하여 loss 값에 저장한다.

그리고 이 비용 최적화 함수를 위해서 옵티마이져를 AdamOptimizer를 정의하여, loss 값을 최적화 하도록 하였다.

 

학습용 모델 정의와, 비용 함수, 옵티마이저 정의가 끝났으면 학습 중간 중간 학습된 모델을 테스트하기 위한 Validation 관련 항목등을 정의한다.

 

   # for validation

   #with tf.name_scope("prediction"):

   validate_image_batch,validate_label_batch,validate_file_batch = read_data_batch(VALIDATION_FILE)

   label_max = tf.argmax(labels,1)

   pre_max = tf.argmax(prediction,1)

   correct_pred = tf.equal(tf.argmax(prediction,1),tf.argmax(labels,1))

   accuracy = tf.reduce_mean(tf.cast(correct_pred,tf.float32))

           

   tf.summary.scalar('accuracy',accuracy)

      

   startTime = datetime.now()

 

학습용 데이타가 아니라 검증용 데이타를 VALIDATION_FILE에서 읽어서 데이타를 validate_image_batch,validate_label_batch,validate_file_batch에 저장한다. 다음, 정확도 체크를 위해서 학습에서 예측된 라벨값과, 학습 데이타용 라벨값을 비교하여 같은지 틀린지를 비교하고, 이를 가지고 평균을 내서 정확도 (accuracy)로 사용한다.

 

학습용 모델과, 테스트용 데이타 등이 준비되었으면 이제 학습을 시작한다.

학습을 시직하기 전에, 학습된 모델을 저장하기 위해서 tf.train.Saver()를 지정한다. 그리고, 그래프로 loss와 accuracy등을 저장하기 위해서 Summary write를 저장한다.

다음 tf.global_variable_initializer()를 수행하여 변수를 초기화 하고, queue에서 데이타를 읽기 위해서 tf.train.Corrdinator를 선언하고 tf.start_queue_runners를 지정하여, queue 러너를 실행한다.

 

   #build the summary tensor based on the tF collection of Summaries

   summary = tf.summary.merge_all()

   

   with tf.Session(config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)) as sess:

       saver = tf.train.Saver() # create saver to store training model into file

       summary_writer = tf.summary.FileWriter(FLAGS.log_dir,sess.graph)

       

       init_op = tf.global_variables_initializer() # use this for tensorflow 0.12rc0

       coord = tf.train.Coordinator()

       threads = tf.train.start_queue_runners(sess=sess, coord=coord)

       sess.run(init_op)

 

변수 초기화와 세션이 준비되었기 때문에 이제 학습을 시작해보자. for 루프를 이용하여 총 10,000 스텝의 학습을 하도록 하였다.

 

       for i in range(10000):

           images_,labels_ = sess.run([image_batch,label_batch])

 

다음 image_batch와 label_batch에서 값을 읽어서 앞에서 정의한 모델에 넣고 train 그래프 (AdamOptimizer를 정의한)를 실행한다.

 

           sess.run(train,feed_dict={images:images_,labels:labels_,keep_prob:0.7})

 

이때 앞에서 읽은 images_와, labels_ 데이타를 피딩하고 keep_prob 값을 0.7로 하여 30% 정도의 값을 Dropout 시킨다.

 

다음 10 스텝 마다 학습 상태를 체크하도록 하였다.

           

           if i % 10 == 0:

               now = datetime.now()-startTime

               print('## time:',now,' steps:',i)         

               

               # print out training status

               rt = sess.run([label_max,pre_max,loss,accuracy],feed_dict={images:images_

                                                         , labels:labels_

                                                         , keep_prob:1.0})

               print ('Prediction loss:',rt[2],' accuracy:',rt[3])

위와 같이 loss 값과 accuracy 값을 받아서 출력하여 현재 모델의 비용 함수 값과 정확도를 측정하고

 

               # validation steps

               validate_images_,validate_labels_ = sess.run([validate_image_batch,validate_label_batch])

               rv = sess.run([label_max,pre_max,loss,accuracy],feed_dict={images:validate_images_

                                                         , labels:validate_labels_

                                                         , keep_prob:1.0})

               print ('Validation loss:',rv[2],' accuracy:',rv[3])

학습용 데이타가 아니라 위와 같이 테스트용 데이타를 피딩하여, 테스트용 데이타로 정확도를 검증한다. 이때 keep_prob를 1.0으로 해서 Dropout 없이 100% 네트워크를 활용한다.

 

               if(rv[3] > 0.9):

                   Break

 

만약에 테스트 정확도가 90% 이상이면 학습을 멈춘다. 그리고 아래와 같이 Summary

 

               # validation accuracy

               summary_str = sess.run(summary,feed_dict={images:validate_images_

                                                         , labels:validate_labels_

                                                         , keep_prob:1.0})

 

               summary_writer.add_summary(summary_str,i)

               summary_writer.flush()

 

마지막으로 다음과 같이 학습이 다된 모델을 saver.save를 이용하여 저장하고, 사용된 리소스들을 정리한다.

       saver.save(sess, 'face_recog') # save session

       coord.request_stop()

       coord.join(threads)

       print('finish')

   

main()

 

이렇게 학습을 끝내면 본인의 경우 약 7000 스텝에서 테스트 정확도 91%로 끝난것을 확인할 수 있다.

 

아래는 텐서보드를 이용하여 학습 과정을 시각화한 내용이다.

 


 

코드는 공개가 가능하지만 학습에 사용한 데이타는 저작권 문제로 공유가 불가능하다. 약 200장의 사진만 제대로 수집을 하면 되기 때문에 각자 수집을 해서 학습을 도전해보는 것을 권장한다. (더 많은 인물에 대한 시도를 해보는것도 좋겠다.)

정리 하며

혹시나 이 튜토리얼을 따라하면서 학습 데이타를 공개할 수 있는 분들이 있다면 다른 분들에게도 많은 도움이 될것이라고 생각한다. 가능하면 데이타가 공개되었으면 좋겠다.

전체 코드는 https://github.com/bwcho75/facerecognition/blob/master/1.%2BFace%2BRecognition%2BTraining.ipynb 에 있다.

그리고 직접 사진을 수집해보면, 데이타 수집 및 가공이 얼마나 어려운지 알 수 있기 때문에 직접 한번 시도해보는 것도 권장한다. 아래는 크롬브라우져 플러그인으로 구글 검색에서 나온 이미지를 싹 긁을 수 있는 플러그인이다. Bulk Download Images (ZIG)

https://www.youtube.com/watch?v=k5ioaelzEBM

 



이 플러그인을 이용하면 손쉽게 특정 인물의 데이타를 수집할 수 있다.

다음 글에서는 학습이 끝난 데이타를 이용해서 실제로 예측을 해보는 부분에 대해서 소개하도록 하겠다.

 

 

 

저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License


Machine Learning Pipeline


조대협 (http://bcho.tistory.com)

대부분 모델 개발과 알고리즘에 집중

머신러닝을 공부하고 나서는 주로 통계학이나, 모델 자체에 많은 공부를 하는 노력을 드렸었다. 선형대수나 미적분 그리고 방정식에 까지 기본으로 돌아가려고 노력을 했었고, 그 중간에 많은 한계에도 부딪혔지만, 김성훈 교수님의 모두를 위한 딥러닝 강의를 접하고 나서, 수학적인 지식도 중요하지만 수학적인 깊은 지식이 없어도 모델 자체를 이해하고 근래에 발전된 머신러닝 개발 프레임웍을 이용하면 모델 개발이 가능하다는 것을 깨달았다.

 

계속해서 모델을 공부하고, 머신러닝을 공부하는 분들을 관심있게 지켜보고 실제 머신러닝을 사용하는 업무들을 살펴보니 재미있는 점이 모두 모델 자체 개발에만 집중한다는 것이다. 커뮤니티에 올라오는 글의 대부분은 어떻게 모델을 구현하는지 어떤 알고리즘을 사용하는지에 대한 내용들이 많았고, 실 업무에 적용하는 분들을 보면 많은 곳들이 R을 이용하여 데이타를 분석하고 모델링을 하는데, 데이타를 CSV 파일 형태로 다운 받아서 정재하고 데이타를 분석하고 모델을 개발하는 곳이 많은 것을 보았다. 데이타의 수집 및 전처리 및 개발된 모델에 대한 서비스에 대해서는 상대적으로 많은 정보를 접하지 못했는데, 예상하기로 대부분 모델 개발에 집중하기 때문이 아닌가 싶다.

 

엔지니어 백그라운드를 가진 나로써는 CSV로 데이타를 끌어다가 정재하고 분석하는 것이 매우 불편해 보이고 이해가 되지 않았다. 빅데이타 분석 시스템에 바로 연결을 하면, CSV로 덤프 받고 업로드 하는 시간등에 대한 고민이 없을텐데.” 왜 그렇게 할까 ?”라는 의문이 계속 생기기 시작하였다.

미니 프로젝트를 시작하다

이런 의문을 가지던중 CNN 네트워크 모델에 대한 대략적인 학습이 끝나고, 실제로 적용하면서 경험을 쌓아보기로 하였다. 그래서 얼굴 인식 모델 개발을 시작하였다. CNN 모델이라는 마법을 사용하면 쉽게 개발이 될줄 알았던 프로젝트가 벌써 몇달이 되어 간다. 학습용 데이타를 구하고, 이를 학습에 적절하도록 전처리 하는 과정에서 많은 실수가 있었고, 그 과정에서 많은 재시도가 있었다.

 

(자세한 내용은 http://bcho.tistory.com/1174 , https://www.slideshare.net/Byungwook/ss-76098082 를 참조)

 

특히나 데이타 자체를 다시 처리해야 하는 일이 많았기 때문에, 데이타 전처리 코드를 지속적으로 개선하였고 개선된 코드를 이용하여 데이타를 지속적으로 다시 처리해서 데이타의 품질을 높여나갔는데, 처리 시간이 계속해서 많이 걸렸다.

자동화와 스케일링의 필요성

특히 이미지 전처리 부분은 사진에서 얼굴이 하나만 있는 사진을 골라내고 얼굴의 각도와 선글라스 유무등을 확인한후 사용 가능한 사진에서 얼굴을 크롭핑하고 학습용 크기로 리사이즈 하는 코드였는데 (자세한 내용 http://bcho.tistory.com/1176) 싱글 쓰레드로 만들다 보니 아무래도 시간이 많이 걸렸다. 실제 운영환경에서는 멀티 쓰레드 또는 멀티 서버를 이용하여 스케일링을 할 필요가 있다고 느꼈다.

 

또한 이미지 수집에서 부터 필터링, 그리고 학습 및 학습된 모델의 배포와 서비스 까지 이 전 과정을 순차적으로 진행을 하되 반복적인 작업이기 때문에 자동화할 필요성이 있다고 생각했다.

아이 체중 예측 모델을 통한 파이프라인에 대한 이해

그러던 중에 팀 동료로 부터 좋은 예제 하나를 전달 받게 되었다.

미국 아기들의 환경에 따른 출생 체중을 예측하는 간단한 선형 회귀 모델을 구현한 파이썬 노트북인데 (https://github.com/GoogleCloudPlatform/training-data-analyst/blob/master/blogs/babyweight/babyweight.ipynb) 하나의 노트북에 전체 단계를 모두 구현해놓았다.

 


 

데이타에 대한 분석을 통한 데이타 특성 추출, 추출된 특성을 통한 모델 개발, 모델 학습을 위한 데이타 전처리 그리고 학습 및 학습된 모델을 통한 예측 서비스 까지 모든 과정을 하나의 노트북에 구현해놓았다.

(시간이 있으면 꼭 보기를 강력 추천한다.)

 

흥미로운 점이 데이타 전처리를 Apache Beam이라는 데이타 처리 플랫폼을 썼고, 그 전처리 코드를 파이썬 노트북에 하나로 다 정리한것이다. (실제로 수행은 로컬에서도 가능하지만, 클라우드에서도 실행이 가능해서 충분한 스케일링을 지원한다.)

 

Apache Beam의 구글의 빅데이타 분석 프레임웍으로 Apache Spark 과 같은 프레임웍이라고 보면된다. Google Dataflow라는 이름으로 구글 클라우드에서 서비스가 되는데, Apache Beam이라는 오픈소스로 공개가 되었다. ( http://bcho.tistory.com/1123 http://bcho.tistory.com/1122 http://bcho.tistory.com/1124 )

 

아 이렇게 하는구나 하는 생각이 들었고, 그즘 실무에서 이와 같은 흐름으로 실제로 머신러닝을 수행하는 것을 볼 기회가 있었다.

데이타 전처리를 스케일링하다.

서비스가 가능한 수준의 전체 머신러닝 서비스 파이프라인을 만들어보고 싶어졌다. 마침 또 Apache Beam의 경우에는 예전에 Java 코드로 실시간 분석을 해본 경험이 있고 이번에 2.0 버전이 릴리즈 되서 이번에는 2.0에서 파이썬을 공부해보기로 하고 개발에 들어갔다.

 

특히 기존의 데이타 전처리 코드는 싱글 쓰레드로 돌기 때문에 스케일링에 문제가 있었지만, Apache Beam을 사용할 경우 멀티 쓰레드 뿐만 아니라 동시에 여러대의 머신에서 돌릴 수 있고 이러한 병렬성에 대해서는 크게 고민을 하지 않아도 Apache Beam이 이 기능을 다 제공해준다. 또한 이 데이타 전처리 코드를 돌릴 런타임도 별도 설치할 필요가 없이 커멘드 하나로 구글 클라우드에서 돌릴 수 가 있다. (직업 특성상 클라우드 자원을 비교적 자유롭게 사용할 수 있었다.)

 

Apache Beam으로 전처리 코드를 컨버팅 한결과 기존 싱글 쓰레드 파이썬 코드가 400~500장의 이미지 전처리에 1~2시간이 걸렸던 반면, 전환후에 대략 15~17분이면 끝낼 수 있었다. 전처리 중에는 서버의 대수가 1대에서 시작해서 부하가 많아지자 자동으로 5대까지 늘어났다. 이제는 아무리 많은 데이타가 들어오더라도 서버의 대수만 단순하게 늘리면 수분~수십분내에 수십,수만장의 데이타 처리가 가능하게 되었다.


<그림. Apache Beam 기반의 이미지 전처리 시스템 실행 화면 >

 

Apache Beam 기반의 이미지 전처리 코드는 https://github.com/bwcho75/facerecognition/blob/master/Preprocess%2Bface%2Brecognition%2Bdata%2Band%2Bgenerate%2Btraining%2Bdata.ipynb 에 공개해 놨다.

 

머신러닝 파이프라인 아키텍쳐와 프로세스

이번 과정을 통해서 머신러닝의 학습 및 예측 시스템 개발이 어느 정도 정형화된 프로세스화가 가능하고 시스템 역시 비슷한 패턴의 아키텍쳐를 사용할 수 있지 않을까 하는 생각이 들었고, 그 내용을 아래와 같이 정리한다.

파이프라인 개발 프로세스

지금까지 경험한 머신러닝 개발 프로세스는 다음과 같다.

 

  1. 데이타 분석
    먼저 머신러닝에 사용할 전체 데이타셋을 분석한다. 그래프도 그려보고 각 변수간의 연관 관계나 분포도를 분석하여, 학습에 사용할 변수를 정의하고 어떤 모델을 사용할지 판단한다.

  2. 모델 정의
    분석된 데이타를 기반으로 모델을 정의하고, 일부 데이타를 샘플링하여 모델을 돌려보고 유효한 모델인지를 체크한다. 모델이 유효하지 않다면 변수와 모델을 바꿔 가면서 최적의 모델을 찾는다.

  3. 데이타 추출 및 전처리
    유효한 모델이 개발이 되면, 일부 데이타가 아니라 전체 데이타를 가지고 학습을 한다. 전체 데이타를 추출해서 모델에 넣어서 학습을 하려면 데이타의 크기가 크면 매번 매뉴얼로 하기가 어렵기 때문에 데이타 추출 및 전처리 부분을 자동화 한다.   

  4. 전체 데이타를 이용한 반복 학습 및 튜닝
    모델 자체가 유효하다고 하더라도 전체 데이타를 가지고 학습 및 검증을 한것이 아니기 때문에 의외의 데이타가 나오거나 전처리에 의해서 필터링되지 않은 데이타가 있을 수 있기 때문에 지속적으로 데이타 추출 및 전처리 모듈을 수정해야 하고, 마찬가지로 모델 역시 정확도를 높이기 위해서 지속적으로 튜닝을 한다. 이 과정에서 전체 데이타를 다루기 때문에 모델 역시 성능을 위해서 분산형 구조로 개선되어야 한다.

  5. 모델 배포
    학습 모델이 완성되었으면 학습된 모델을 가지고 예측을 할 수 있는 시스템을 개발하고 이를 배포한다.

  6. 파이프라인 연결 및 자동화
    머신러닝의 모델은 위의 과정을 통해서 만들었지만, 데이타가 앞으로도 지속적으로 들어올 것이고 지속적인 개선이 필요하기 때문에 이 전과정을 자동화 한다. 이때 중요한것은 데이타 전처리, 학습, 튜닝, 배포등의 각 과정을 물 흐르듯이 연결하고 자동화를 해야 하는데 이렇게 데이타를 흐르는 길을 데이타 플로우라고 한다. (흔히 Luigi, Rundeck, Airflow와 같은 데이타플로우 오케스트레이션 툴을 이용한다)

 

전체적인 프로세스에 대해서 좋은 영상이 있어서 공유한다.


아키텍쳐

위의 프로세스를 기반으로한 머신러닝 파이프라인 아키텍쳐 는 다음과 같다.


 

 

Inputs

머신 러닝 파이프라인의 가장 처음단은 데이타를 수집하고 이 수집된 데이타를 저장하는 부분이다.

데이타 수집은 시간,일,주,월과 같이 주기적으로 데이타를 수집하는 배치 프로세싱과, 실시간으로 데이타를 수집하는 리얼타임 프로세싱 두가지로 나뉘어 진다. 이 두 파이프라인을 통해서 데이타 소스로 부터 데이타를 수집하고 필터링하고 정재하여, 데이타 레이크에 저장한다. 이 구조는 일반적인 빅데이타 분석 시스템의 구조와 유사하다. (참고 자료 http://bcho.tistory.com/984 http://bcho.tistory.com/671 )

 

개인적으로 머신러닝을 위해서 중요한 부분 중 하나는 데이타 레이크를 얼마나 잘 구축하느냐이다. 데이타 레이크는 모든 데이타가 모여 있는 곳으로 보통 데이타 레이크를 구축할때는 많은 데이타를 모으는 데만 집중하는데, 데이타를 잘 모으는 것은 기본이고 가장 중요한 점은 이 모여 있는 데이타에 대한 접근성을 제공하는 것이다.

 

무슨 이야기인가 하면, 보통 머신러닝 학습을 위해서 학습 데이타를 받거나 또는 데이타에 대한 연관성 분석등을 하기 위해서는 데이타 레이크에서 데이타를 꺼내오는데, 데이타 레이크를 개발 운영 하는 사람과 데이타를 분석하고 머신러닝 모델을 만드는 사람은 보통 다르기 때문에, 모델을 만드는 사람이 데이타 레이크를 운영하는 사람에게 “무슨 무슨 데이타를 뽑아서 CSV로 전달해 주세요.” 라고 이야기 하는 것이 보통이다. 그런데 이 과정이 번거롭기도 하고 시간이 많이 걸린다.

가장 이상적인 방법은 데이타를 분석하고 모델링 하는 사람이 데이타 레이크 운영팀에 부탁하지 않고서도 손쉽고 빠르게 데이타에 접근해서 데이타를 읽어오고 분석을 할 수 있어야 한다.

직업 특성상 구글의 빅쿼리를 많이 접하게 되는데, 빅쿼리는 대용량 데이타를 저장할 수 있을 뿐만 아니라 파이썬 노트북이나 R 스튜디오 플러그인을 통해서 바로 데이타를 불러와서 분석할 수 있다.  


<그림 INPUT 계층의 빅데이타 저장 분석 아키텍쳐>

Pre processing & Asset creation

Pre processing은 수집한 데이타를 학습 시스템에 넣기 위해서 적절한 데이타만 필터링하고 맞는 포맷으로 바꾸는 작업을 한다. 작은 모델이나 개발등에서는 샘플링된 데이타를 로컬에서 내려 받아서 R이나 numpy/pandas등으로 작업이 가능하지만, 데이타가 수테라에서 수백테라이상이 되는 빅데이타라면 로컬에서는 작업이 불가능하기 때문에, 데이타 전처리 컴포넌트를 만들어야 한다.

일반적으로 빅데이타 분석에서 사용되는 기술을 사용하면 되는데, 배치성 전처리는 하둡이나 스파크와 같은 기술이 보편적으로 사용되고 실시간 스트리밍 분석은 스파크 스트리밍등이 사용된다.


Train

학습은 전처리된 데이타를 시스템에 넣어서 모델을 학습 시키는 단계이다. 이 부분에서 생각해야 할점은 첫번째는 성능 두번째는 튜닝이다. 성능 부분에서는 GPU등을 이용하여 학습속도를 늘리고 여러대의 머신을 연결하여 학습을 할 수 있는 병렬성이 필요하다. 작은 모델의 경우에는 수시간에서 하루 이틀 정도 소요되겠지만 모델이 크면 한달 이상이 걸리기 때문에 고성능 하드웨어와 병렬 처리를 통해서 학습 시간을 줄이는 접근이 필요하다. 작은 모델의 경우에는 NVIDIA GPU를 데스크탑에 장착해놓고 로컬에서 돌리는 것이 가성비 적으로 유리하고, 큰 모델을 돌리거나 동시에 여러 모델을 학습하고자 할때는 클라우드를 사용하는 것이 절대 적으로 유리하다 특히 구글 클라우드의 경우에는  알파고에서 사용된 GPU의 다음 세대인 TPU (텐서플로우 전용 딥러닝 CPU)를 제공한다. https://cloud.google.com/tpu/ CPU나 GPU대비 최대 15~30배 정도의 성능 차이가 난다.

 

 

학습 단계에서는 세부 변수를 튜닝할 필요가 있는데, 예를 들어 학습 속도나 뉴럴 네트워크의 폭이나 깊이, 드롭 아웃의 수, 컨볼루셔널 필터의 크기등등이 있다. 이러한 변수들을 하이퍼 패러미터라고 하는데, 학습 과정에서 모델의 정확도를 높이기 위해서 이러한 변수들을 자동으로 튜닝할 수 있는 아키텍쳐를 가지는 것이 좋다.

 

텐서플로우등과 같은 머신러닝 전용 프레임웍을 사용하여 직접 모델을 구현하는 방법도 있지만, 모델의 난이도가 그리 높지 않다면 SparkML등과 같이 미리 구현된 모델의 런타임을 사용하는 방법도 있다.

Predict

Predict에서는 학습된 모델을 이용하여 예측 기능을 서비스 하는데, 텐서플로우에서는  Tensorflow Serv를 사용하면 되지만, Tensorflow Serv의 경우에는 bazel 빌드를 이용하여 환경을 구축해야 하고, 대규모 서비를 이용한 분산 환경 서비스를 따로 개발해야 한다. 거기다가 인터페이스가 gRPC이다. (귀찮다.)

구글 CloudML의 경우에는 별도의 빌드등도 필요 없고 텐서 플로우 모델만 배포하면 대규모 서비스를 할 수 있는 런타임이 바로 제공되고 무엇보다 gRPC 인터페이스뿐만 아니라 HTTP/REST 인터페이스를 제공한다. 만약에 Production에서 머신러닝 모델을 서비스하고자 한다면 구글 CloudML을 고려해보기를 권장한다.

Dataflow Orchestration

이 전과정을 서로 유기적으로 묶어 주는 것을 Dataflow Orchestration이라고 한다.

예를 들어 하루에 한번씩 이 파이프라인을 실행하도록 하고, 파이프라인에서 데이타 전처리 과정을 수행하고, 이 과정이 끝나면 자동으로 학습을 진행하고 학습 정확도가 정해진 수준을 넘으면 자동으로 학습된 모델은 서비스 시스템에 배포하는 이 일련의 과정을 자동으로 순차적으로 수행할 수 있도록 엮어 주는 과정이다.

airbnb에서 개발한 Airflow나 luigi 등의 솔루션을 사용하면 된다.

아직도 갈길은 멀다.

얼굴 인식이라는 간단한 모델을 개발하고 있지만, 전체를 자동화 하고, 클라우드 컴퓨팅을 통해서 학습 시간을 단축 시키고 예측 서비스를 할 수 있는 컴포넌트를 개발해야 하고, 향후에는 하이퍼 패러미터 튜닝을 자동으로 할 수 있는 수준까지 가보려고 한다. 그 후에는 GAN을 통한 얼굴 합성들도 도전하려고 하는데, node.js 공부하는데도 1~2년을 투자한후에나 조금이나마 이해할 수 있게 되었는데, 머신러닝을 시작한지 이제 대략 8개월 정도. 길게 보고 해야 하겠다.

 



 

 

저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

연예인 얼굴 인식 서비스를 만들어보자 #1 - 데이타 준비하기

 

CNN 에 대한 이론 공부와 텐서 플로우에 대한 기본 이해를 끝내서 실제로 모델을 만들어보기로 하였다.

CNN을 이용한 이미지 인식중 대중적인 주제로 얼굴 인식 (Face recognition)을 주제로 잡아서, 이 모델을 만들기로 하고 아직 실력이 미흡하여 호주팀에서 일하고 있는 동료인 Win woo 라는 동료에게 모델과 튜토리얼 개발을 부탁하였다.

 

이제 부터 연재하는 연예인 얼굴 인식 서비스는 Win woo 가 만든 코드를 기반으로 하여 설명한다. (코드 원본 주소 : https://github.com/wwoo/tf_face )

 

얼굴 데이타를 구할 수 있는곳

먼저 얼굴 인식 모델을 만들려면, 학습을 시킬 충분한 데이타가 있어야 한다. 사람 얼굴을 일일이 구할 수 도 없고, 구글이나 네이버에서 일일이 저장할 수 도 없기 때문에, 공개된 데이타셋을 활용하였는데, PubFig (Public Figures Face Database - http://www.cs.columbia.edu/CAVE/databases/pubfig/) 를 사용하였다.


 

이 데이타셋에는 약 200명에 대한 58,000여장의 이미지를 저장하고 있는데, 이 중의 일부만을 사용하였다.

Download 페이지로 가면, txt 파일 형태 (http://www.cs.columbia.edu/CAVE/databases/pubfig/download/dev_urls.txt) 로 아래와 같이

 

Abhishek Bachan 1 http://1.bp.blogspot.com/_Y7rzCyUABeI/SNIltEyEnjI/AAAAAAAABOg/E1keU_52aFc/s400/ash_abhishek_365x470.jpg 183,60,297,174 f533da9fbd1c770428c8961f3fa48950
Abhishek Bachan 2 http://1.bp.blogspot.com/_v9nTKD7D57Q/SQ3HUQHsp_I/AAAAAAAAQuo/DfPcHPX2t_o/s400/normal_14thbombaytimes013.jpg 49,71,143,165 e36a8b24f0761ec75bdc0489d8fd570b
Abhishek Bachan 3 http://2.bp.blogspot.com/_v9nTKD7D57Q/SL5KwcwQlRI/AAAAAAAANxM/mJPzEHPI1rU/s400/ERTYH.jpg 32,68,142,178 583608783525c2ac419b41e538a6925d

 

사람이름, 이미지 번호, 다운로드 URL, 사진 크기, MD5 체크섬을 이 필드로 저장되어 있다.

이 파일을 이용하여 다운로드 URL에서 사진을 다운받아서, 사람이름으로된 폴더에 저장한다.

물론 수동으로 할 수 없으니 HTTP Client를 이용하여, URL에서 사진을 다운로드 하게 하고, 이를 사람이름 폴더 별로 저장하도록 해야 한다.

 

HTTP Client를 이용하여 파일을 다운로드 받는 코드는 일반적인 코드이기 때문에 별도로 설명하지 않는다.

본인의 경우에는 Win이 만든 https://github.com/wwoo/tf_face/blob/master/tf/face_extract/pubfig_get.py 코드를 이용하여 데이타를 다운로드 받았다.

사용법은  https://github.com/wwoo/tf_face 에 나와 있는데,

 

$> python tf/face_extract/pubfig_get.py tf/face_extract/eval_urls.txt ./data

를 실행하면 ./data 디렉토리에 이미지를 다운로드 받아서 사람 이름별 폴더에 저장해준다.

evals_urls.txt에는 위에서 언급한 dev_urls.txt 형태의 데이타가 들어간다.


사람 종류가 너무 많으면 데이타를 정재하는 작업이 어렵고, (왜 어려운지는 뒤에 나옴) 학습 시간이 많이 걸리기 때문에, 약 47명의 데이타를 다운로드 받아서 작업하였다.

학습 데이타 준비에 있어서 경험

쓰레기 데이타 골라내기

데이타를 다운받고 나니, 아뿔사!! PubFig 데이타셋이 오래되어서 없는 이미지도 있고 학습에 적절하지 않은 이미지도 있다.


주로 학습에 적절하지 않은 데이타는 한 사진에 두사람 이상의 얼굴이 있거나, 이미지가 사라져서 위의 우측 그림처럼, 이미지가 없는 형태로 나오는 경우인데, 이러한 데이타는 어쩔 수 없이 눈으로 한장한장 다 걸러내야만 했는데, 이런 간단한 데이타 필터링 처리는 Google Cloud Vision API를 이용하여, 얼굴이 하나만 있는 사진만을 사용하도록 하여 필터링을 하였다.

학습 데이타의 분포

처음에 학습을 시작할때, 분류별로 데이타의 수를 다르게 하였다. 어렵게 모은 데이타를 버리기가 싫어서 모두 다 넣고 학습 시켰는데, 그랬더니 학습이 쏠리는 현상이 발생하였다.

예를 들어 안젤리나 졸리 300장, 브래드피트 100장, 제시카 알바 100장 이런식으로 학습을 시켰더니, 이미지 예측에서 안젤리나 졸리로 예측하는 경우가 많아졌다. 그래서 학습을 시킬때는 데이타수가 작은 쪽으로 맞춰서 각 클래스당 학습 데이타수가 같도록 하였다. 즉 위의 데이타의 경우에는 안젤리나 졸리 100장, 브래드피트 100장, 제시카 알바 100장식으로 데이타 수를 같게 해야했다.

라벨은 숫자로

라벨의 가독성을 높이기 위해서 라벨을 영문 이름으로 사용했는데, CNN 알고리즘에서 최종 분류를 하는 알고리즘은 softmax 로 그 결과 값을 0,1,2…,N식으로 라벨을 사용하기 때문에, 정수형으로 변환을 해줘야 하는데, 텐서 플로우 코드에서는 이게 그리 쉽지않았다. 그래서 차라리 처음 부터 학습 데이타를 만들때는 라벨을 정수형으로 만드는것이 더 효과적이다

얼굴 각도, 표정,메이크업, 선글라스 도 중요하다

CNN 알고리즘을 마법처럼 생각해서였을까? 데이타만 있다면 어떻게든 학습이 될 줄 알았다. 그러나 얼굴의 각도가 많이 다르거나 표정이 심하게 차이가 난 경우에는 다른 사람으로 인식이 되기 때문에 가능하면 비슷한 표정에 비슷한 각도의 사진으로 학습 시키는 것이 정확도를 높일 수 있다.


 

얼굴 각도의 경우 구글 클라우드 VISION API를 이용하면 각도를 추출할 수 있기 때문에 20도 이상 차이가 나는 사진은 필터링 하였고, 표정 부분도 VISION API를 이용하면 감정도를 분석할 수 있기 때문에 필터링이 가능하다. (아래서 설명하는 코드에서는 감정도 분석 부분은 적용하지 않았다)

또한 선글라스를 쓴 경우에도 다른 사람으로 인식할 수 있기 때문에 VISION API에서 물체 인식 기능을 이용하여 선글라스가 검출된 경우에는 학습 데이타에서 제거하였다.

이외에도 헤어스타일이나 메이크업이 심하게 차이가 나는 경우에는 다른 사람으로 인식되는 확률이 높기 때문에 이런 데이타도 가급적이면 필터링을 하는것이 좋다.

웹 크라울링의 문제점

데이타를 쉽게 수집하려고 웹 크라울러를 이용해서 구글 이미지 검색에서 이미지를 수집해봤지만, 정확도는 매우 낮게 나왔다.


 

https://www.youtube.com/watch?v=k5ioaelzEBM

<그림. 설현 얼굴을 웹 크라울러를 이용하여 수집하는 화면>

 

아래는 웹 크라울러를 이용하여 EXO 루한의 사진을 수집한 결과중 일부이다.


웹크라울러로 수집한 데이타는, 앞에서 언급한 쓰레기 데이타들이 너무 많다. 메이크업, 표정, 얼굴 각도, 두명 이상 있는 사진들이 많았고, 거기에 더해서 그 사람이 아닌 사람의 얼굴 사진까지 같이 수집이 되는 경우가 많았다.

웹 크라울링을 이용한 학습 데이타 수집은 적어도 얼굴 인식용 데이타 수집에 있어서는 좋은 방법은 아닌것 같다. 혹여나 웹크라울러를 사용하더라도 반드시 수동으로 직접 데이타를 검증하는 것이 좋다.

학습 데이타의 양도 중요하지만 질도 매우 중요하다

아이돌 그룹인 EXO와 레드벨벳의 사진을 웹 크라울러를 이용해서 수집한 후에 학습을 시켜보았다. 사람당 약 200장의 데이타로 8개 클래스 정도를 테스트해봤는데 정확도가 10%가 나오지를 않았다.

대신 데이타를 학습에 좋은 데이타를 일일이 눈으로 확인하여 클래스당 30장 정도를 수집해서 학습 시킨 결과 60% 정도의 정확도를 얻을 수 있었다.  양도 중요하지만 학습 데이타의 질적인 면도 중요하다.

중복데이타 처리 문제

데이타를 수집해본 결과, 중복되는 데이타가 생각보다 많았다. 중복 데이타를 걸러내기 위해서 파일의 MD5 해쉬 값을 추출해낸 후 이를 비교해서 중복되는 파일을 제거하였는데, 어느정도 효과를 볼 수 있었지만, 아래 이미지와 같이 같은 이미지지만, 편집이나 리사이즈가 된 이미지의 경우에는 다른 파일로 인식되서 중복 체크에서 검출되지 않았다.


연예인 얼굴 인식은 어렵다

얼굴 인식 예제를 만들면서 재미를 위해서 한국 연예인 얼굴을 수집하여 학습에 사용했는데, 제대로 된 학습 데이타를 구하기가 매우 어려웠다. 앞에서 언급한데로 메이크업이나 표정 변화가 너무 심했고, 어렸을때나 나이먹었을때의 차이등이 심했다. 간단한 공부용으로 사용하기에는 좋은 데이타는 아닌것 같다.

그러면 학습에 좋은 데이타는?

그러면 얼굴 인식 학습에 좋은 데이타는 무엇일까? 테스트를 하면서 내린 자체적인 결론은 정면 프로필 사진류가 제일 좋다. 특히 스튜디오에서 찍은 사진은 같은 조명에 같은 메이크업과 헤어스타일로 찍은 경우가 많기 때문에 학습에 적절하다. 또는 동영상의 경우에는 프레임을 잘라내면 유사한 표정과 유사한 각도, 조명등에 대한 데이타를 많이 얻을 수 있기 때문에 좋은 데이타 된다.

얼굴 추출하기

그러면 앞의 내용을 바탕으로 해서, 적절한 학습용 얼굴 이미지를 추출하는 프로그램을 만들어보자

포토샵으로 일일이 할 수 없기 때문에 얼굴 영역을 인식하는 API를 사용하기로한다. OPEN CV와 같은 오픈소스 라이브러리를 사용할 수 도 있지만 구글의 VISION API의 경우 얼굴 영역을 아주 잘 잘라내어주고,  얼굴의 각도나 표정을 인식해서 필터링 하는 기능까지 코드 수십줄만 가지고도 구현이 가능했기 때문에, VISION API를 사용하였다. https://cloud.google.com/vision/

VISION API ENABLE 하기

VISION API를 사용하기 위해서는 해당 구글 클라우드 프로젝트에서 VISION API를 사용하도록 ENABLE 해줘야 한다.

VISION API를 ENABLE하기 위해서는 아래 화면과 같이 구글 클라우드 콘솔 > API Manager 들어간후


 

+ENABLE API를 클릭하여 아래 그림과 같이 Vision API를 클릭하여 ENABLE 시켜준다.

 



 

SERVICE ACCOUNT 키 만들기

다음으로 이 VISION API를 호출하기 위해서는 API 토큰이 필요한데, SERVICE ACCOUNT 라는 JSON 파일을 다운 받아서 사용한다.

구글 클라우드 콘솔에서 API Manager로 들어간후 Credentials 메뉴에서 Create creadential 메뉴를 선택한후, Service account key 메뉴를 선택한다


 

다음 Create Service Account key를 만들도록 하고, accountname과 id와 같은 정보를 넣는다. 이때 중요한것이 이 키가 가지고 있는 사용자 권한을 설정해야 하는데, 편의상 모든 권한을 가지고 있는  Project Owner 권한으로 키를 생성한다.

 

(주의. 실제 운영환경에서 전체 권한을 가지는 키는 보안상의 위험하기 때문에 특정 서비스에 대한 접근 권한만을 가지도록 지정하여 Service account를 생성하기를 권장한다.)

 


 

Service account key가 생성이 되면, json 파일 형태로 다운로드가 된다.

여기서는 terrycho-ml-80abc460730c.json 이름으로 저장하였다.

 

예제 코드

그럼 예제를 보자 코드의 전문은 https://github.com/bwcho75/facerecognition/blob/master/com/terry/face/extract/crop_face.py 에 있다.

 

이 코드는 이미지 파일이 있는 디렉토리를 지정하고, 아웃풋 디렉토리를 지정해주면 이미지 파일을 읽어서 얼굴이 있는지 없는지를 체크하고 얼굴이 있으면, 얼굴 부분만 잘라낸 후에, 얼굴 사진을 96x96 사이즈로 리사즈 한후에,

70%의 파일들은 학습용으로 사용하기 위해서 {아웃풋 디렉토리/training/} 디렉토리에 저장하고

나머지 30%의 파일들은 검증용으로 사용하기 위해서 {아웃풋 디렉토리/validate/} 디렉토리에 저장한다.

 

그리고 학습용 파일 목록은 다음과 같이 training_file.txt에 파일 위치,사람명(라벨) 형태로 저장하고

/Users/terrycho/traning_datav2/training/wsmith.jpg,Will Smith

/Users/terrycho/traning_datav2/training/wsmith061408.jpg,Will Smith

/Users/terrycho/traning_datav2/training/wsmith1.jpg,Will Smith

 

검증용 파일들은 validate_file.txt에 마찬가지로  파일위치와, 사람명(라벨)을 저장한다.

사용 방법은 다음과 같다.

python com/terry/face/extract/crop_face.py “원본 파일이있는 디렉토리" “아웃풋 디렉토리"

(원본 파일 디렉토리안에는 {사람이름명} 디렉토리 아래에 사진들이 쭈욱 있는 구조라야 한다.)

 

자 그러면, 코드의 주요 부분을 살펴보자

 

VISION API 초기화 하기

  def __init__(self):

       # initialize library

       #credentials = GoogleCredentials.get_application_default()

       scopes = ['https://www.googleapis.com/auth/cloud-platform']

       credentials = ServiceAccountCredentials.from_json_keyfile_name(

                       './terrycho-ml-80abc460730c.json', scopes=scopes)

       self.service = discovery.build('vision', 'v1', credentials=credentials)

 

초기화 부분은 Google Vision API를 사용하기 위해서 OAuth 인증을 하는 부분이다.

scope를 googleapi로 정해주고, 인증 방식을 Service Account를 사용한다. credentials 부분에 service account key 파일인 terrycho-ml-80abc460730c.json를 지정한다.

 

얼굴 영역 찾아내기

다음은 이미지에서 얼굴을 인식하고, 얼굴 영역(사각형) 좌표를 리턴하는 함수를 보자

 

   def detect_face(self,image_file):

       try:

           with io.open(image_file,'rb') as fd:

               image = fd.read()

               batch_request = [{

                       'image':{

                           'content':base64.b64encode(image).decode('utf-8')

                           },

                       'features':[

                           {

                           'type':'FACE_DETECTION',

                           'maxResults':MAX_FACE,

                           },

                           {

                           'type':'LABEL_DETECTION',

                           'maxResults':MAX_LABEL,

                           }

                                   ]

                       }]

               fd.close()

       

           request = self.service.images().annotate(body={

                           'requests':batch_request, })

           response = request.execute()

           if 'faceAnnotations' not in response['responses'][0]:

                print('[Error] %s: Cannot find face ' % image_file)

                return None

               

           face = response['responses'][0]['faceAnnotations']

           label = response['responses'][0]['labelAnnotations']

           

           if len(face) > 1 :

               print('[Error] %s: It has more than 2 faces in a file' % image_file)

               return None

           

           roll_angle = face[0]['rollAngle']

           pan_angle = face[0]['panAngle']

           tilt_angle = face[0]['tiltAngle']

           angle = [roll_angle,pan_angle,tilt_angle]

           

           # check angle

           # if face skew angle is greater than > 20, it will skip the data

           if abs(roll_angle) > MAX_ROLL or abs(pan_angle) > MAX_PAN or abs(tilt_angle) > MAX_TILT:

               print('[Error] %s: face skew angle is big' % image_file)

               return None

           

           # check sunglasses

           for l in label:

               if 'sunglasses' in l['description']:

                 print('[Error] %s: sunglass is detected' % image_file)  

                 return None

           

           box = face[0]['fdBoundingPoly']['vertices']

           left = box[0]['x']

           top = box[1]['y']

               

           right = box[2]['x']

           bottom = box[2]['y']

               

           rect = [left,top,right,bottom]

               

           print("[Info] %s: Find face from in position %s and skew angle %s" % (image_file,rect,angle))

           return rect

       except Exception as e:

           print('[Error] %s: cannot process file : %s' %(image_file,str(e)) )

           

 

 

맨 처음에는 얼굴 영역을 추출하기전에, 같은 파일이 예전에 사용되었는지를 확인한다.

           image = Image.open(fd)  

 

           # extract hash from image to check duplicated image

           m = hashlib.md5()

           with io.BytesIO() as memf:

               image.save(memf, 'PNG')

               data = memf.getvalue()

               m.update(data)

 

           if image_hash in global_image_hash:

               print('[Error] %s: Duplicated image' %(image_file) )

               return None

           global_image_hash.append(image_hash)

 

이미지에서 md5 해쉬를 추출한후에, 이 해쉬를 이용하여 학습 데이타로 사용된 파일들의 해쉬와 비교한다. 만약에 중복되는 것이 없으면 이 해쉬를 리스트에 추가하고 다음 과정을 수행한다.

 

VISION API를 이용하여, 얼굴 영역을 추출하는데, 위의 코드에서 처럼 image_file을 읽은후에, batch_request라는 문자열을 만든다. JSON 형태의 문자열이 되는데, 이때 image라는 항목에 이미지 데이타를 base64 인코딩 방식으로 인코딩해서 전송한다. 그리고 VISION API는 얼굴인식뿐 아니라 사물 인식, 라벨인식등 여러가지 기능이 있기 때문에 그중에서 타입을 ‘FACE_DETECTION’으로 정의하여 얼굴 영역만 인식하도록 한다.

 

request를 만들었으면, VISION API로 요청을 보내면 응답이 오는데, 이중에서 response 엘리먼트의 첫번째 인자 ( [‘responses’][0] )은 첫번째 얼굴은 뜻하는데, 여기서 [‘faceAnnotation’]을 하면 얼굴에 대한 정보만을 얻을 수 있다. 이중에서  [‘fdBoundingPoly’] 값이 얼굴 영역을 나타내는 사각형이다. 이 갑ㄱㅅ을 읽어서 left,top,right,bottom 값에 세팅한 후 리턴한다.

 

그리고 얼굴의 각도 (상하좌우옆)를 추출하여, 얼국 각도가 각각 20도 이상 더 돌아간 경우에는 학습 데이타로 사용하지 않고 필터링을 해냈다.

다음은 각도를 추출하고 필터링을 하는 부분이다.

           roll_angle = face[0]['rollAngle']

           pan_angle = face[0]['panAngle']

           tilt_angle = face[0]['tiltAngle']

           angle = [roll_angle,pan_angle,tilt_angle]

           

           # check angle

           # if face skew angle is greater than > 20, it will skip the data

           if abs(roll_angle) > MAX_ROLL or abs(pan_angle) > MAX_PAN or abs(tilt_angle) > MAX_TILT:

               print('[Error] %s: face skew angle is big' % image_file)

               return None

 

 

VISION API에서 추가로 “FACE DETECTION” 뿐만 아니라 “LABEL_DETECTION” 을 같이 수행했는데 이유는 선글라스를 쓰고 있는 사진을 필터링하기 위해서 사용하였다. 아래는 선글라스 있는 사진을 검출하는  코드이다.

           # check sunglasses

           for l in label:

               if 'sunglasses' in l['description']:

                 print('[Error] %s: sunglass is detected' % image_file)  

                 return None

 

얼굴 잘라내고 리사이즈 하기

앞의 detect_face에서 필터링하고 찾아낸 얼굴 영역을 가지고 그 부분만 전체 사진에서 잘라내고, 잘라낸 얼굴을 학습에 적합하도록 같은 크기 (96x96)으로 리사이즈 한다.

이런 이미지 처리를 위해서 PIL (Python Imaging Library - http://www.pythonware.com/products/pil/)를 사용하였다.

   def crop_face(self,image_file,rect,outputfile):

       try:

           fd = io.open(image_file,'rb')

           image = Image.open(fd)  

           crop = image.crop(rect)

           im = crop.resize(IMAGE_SIZE,Image.ANTIALIAS)

           im.save(outputfile,"JPEG")

           fd.close()

           print('[Info] %s: Crop face %s and write it to file : %s' %(image_file,rect,outputfile) )

       except Exception as e:

           print('[Error] %s: Crop image writing error : %s' %(image_file,str(e)) )

image_file을 인자로 받아서 , rect 에 정의된 사각형 영역 만큼 crop를 해서 잘라내고, resize 함수를 이용하여 크기를 96x96으로 조정한후 (참고 IMAGE_SIZE = 96,96 로 정의되어 있다.) outputfile 경로에 저장하게 된다.        

 

실행을 해서 정재된 데이타는 다음과 같다.



  

생각해볼만한점들

이 코드는 간단한 토이 프로그램이기 때문에 간단하게 작성했지만 실제 운영환경에 적용하기 위해서는 몇가지 고려해야 할 사항이 있다.

먼저, 이 코드는 싱글 쓰레드로 돌기 때문에 속도가 상대적으로 느리다 그래서 멀티 쓰레드로 코드를 수정할 필요가 있으며, 만약에 수백만장의 사진을 정재하기 위해서는 한대의 서버로 되지 않기 때문에, 원본 데이타를 여러 서버로 나눠서 처리할 수 있는 분산 처리 구조가 고려되어야 한다.

또한, VISION API로 사진을 전송할때는 BASE64 인코딩된 구조로 서버에 이미지를 직접 전송하기 때문에, 자칫 이미지 사이즈들이 크면 네트워크 대역폭을 많이 잡아먹을 수 있기 때문에 가능하다면 식별이 가능한 크기에서 리사이즈를 한 후에, 서버로 전송하는 것이 좋다. 실제로 필요한 얼굴 크기는 96x96 픽셀이기 때문에 필요없이 1000만화소 고화질의 사진들을 전송해서 네트워크 비용을 낭비하지 않기를 바란다.

 

다음은 이렇게 정재한 파일들을 텐서플로우에서 읽어서 실제로 학습하는 모델을 만들어보겠다.


위의 코드를 멀티 프로세스&멀티쓰레드로 돌리는 아키텍쳐와 코드는 http://bcho.tistory.com/1177 글을 참고하기 바란다.

 

저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

얼굴 인식 모델을 CNN 네트워크를 이용하여 학습을 시키고 있는데, 지금까지 최대 정확도가 60% 정도밖에 나오지 않는다.

오히려 dropout을 0.5 비율로 적용했더니, 정확도가 30% 정도로 떨어져 버렸다.


원인을 찾던중에 네트워크보다는 학습 데이타에 문제가 있을 것으로 추정된다.

학습 데이타의 양이 그리 많지 않은데 (4 클래스, 사람당 120장), 데이타가 깔끔하지 않다.

아래와 같은 데이타가 문제가 되는데

  • 회전각이 너무 큰경우
  • 선글라스를 쓴 경우
  • 화장이나 염색
  • 사진이 그레이 스케일 처리가 되어 있거나 특정 색상이 들어간경우 


그래서 데이타 클랜징 모듈에서 구글 클라우드 비젼 API를 이용하여 회전각이 20도 가 넘은 경우 제외하고, 라벨 인식을 이용하여 선글라스가 있을 경우도 데이타에서 제외했다. 다음은 학습후에 효과를 적는걸로

이렇게 튜닝 한 결과, 검증 정확도가 대략 80% 내외가 나온다.
이번에는 데이타를 더 모아서, 인당 200장 정도의 데이타로 학습을 시도해서 정확도를 높여볼 예정인데.

가만 보니, 얼굴 표정에 따라서도 오차가 꽤 있을 것으로 보인데, 심하게 찡그리는 얼굴들. 만약에 표정변화도를 비전 API에서 감정도로 받아들인후에, 이를 기반으로 범위를 정해서 필터 아웃 시키는 것을 만들어볼 필요가 있을듯.



데이타가 모자라서 웹에서 크라울링을 하고 나니, 중복되는 이미지가 많아져서 학습 데이타가 또 정확해지지 않는 현상이 생겼다.

그래서 크롭된 이미지에서 MD5 해쉬를 추출하여 저장하고, 이를 비교하는 방식으로 개선을 하였지만, 웹의 이미지의 특성상 같은 이미지라도 올리는 사람이 편집을해서 색이나 명도등을 바꾼 경우에는 같은 이미지이기는 하지만, 바이너리 데이타상으로는 다른 이미지이기 때문에 중복 체크 로직에 걸리지 않고 통과하는 현상을 보였다.





저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

연예인 얼굴 인식 서비스를 만들어보자 #2


CSV 목록에 있는 이미지 데이타를 읽어보자


조대협 (http://bcho.tistory.com)


앞의 글(http://bcho.tistory.com/1166) 에서는 얼굴 인식 데이타를 확보하고, 전처리를 통해서 96x96 사이즈로 만드는 것을 살펴보았다.

그러면, 이 전처리가 끝난 데이타를 텐서플로우에서 학습용으로 쓰기 위해서 데이타를 읽어 들이는 것을 살펴보겠다.


파일에서 학습데이타를 읽는 방법과 큐에 대한 설명은 아래 두 글을 참고하기 바란다.

http://bcho.tistory.com/1165

http://bcho.tistory.com/1163

파일 포맷

파일 포맷은 다음과 같다

/Users/terrycho/traning_datav2/training/007BIL_Aaron_Eckhart_001.jpg,Aaron Eckhart

/Users/terrycho/traning_datav2/training/08486023.jpg,Aaron Eckhart

/Users/terrycho/traning_datav2/training/09.jpg,Aaron Eckhart

/Users/terrycho/traning_datav2/training/0_61_091107_411.jpg,Aaron Eckhart


‘,’로 구분되는 CSV 형태의 파일 포맷이며, 앞에는 이미지의 경로, 뒤에는 해당 이미지의 라벨이 명시되어 있다.


예제 코드

예제코드를 살펴보자

예제 코드의 형태는 http://bcho.tistory.com/1165 에 소개된 CSV 파일을 읽는 코드와 크게 드리지 않다.


import tensorflow as tf

import numpy as np

import matplotlib.pyplot as plt


csv_file =  tf.train.string_input_producer(['/Users/terrycho/dev/ws_gae_demo/terry-face-recog/training_file.txt']

                                               ,name='filename_queue')

textReader = tf.TextLineReader()

_,line = textReader.read(csv_file)

imagefile,label = tf.decode_csv(line,record_defaults=[ [""],[""] ])

image = tf.image.decode_jpeg(tf.read_file(imagefile),channels=3)



with tf.Session() as sess:

   

   coord = tf.train.Coordinator()

   threads = tf.train.start_queue_runners(sess=sess, coord=coord)

   

   for i in range(100):

       image_value,label_value,imagefile_value = sess.run([image,label,imagefile])

       plt.imshow(image_value)

       plt.show()

       print label_value,":",imagefile_value

   

   coord.request_stop()

   coord.join(threads)


특별한 부분만 살펴보자면

imagefile,label = tf.decode_csv(line,record_defaults=[ [""],[""] ])

image = tf.image.decode_jpeg(tf.read_file(imagefile),channels=3)

부분인데, TextReader로 읽어드린 문자열을 파싱해서 이미지 파일명 (imagefile)과 라벨(label)로 추출하고

이 imagefile을가지고, tf.image.decode_jpeg 메서드를 이용하여 jpeg  파일을 읽어서 텐서형으로 바꾼다. 이때, channel=3 으로 설정하였는데, 이유는 이 이미지는 칼라 이미지로 RGB 3개의 값을 가지기 때문에 3차원으로 정의하였다.


다음 텐서 플로우 세션을 시작한 다음에

image_value,label_value,imagefile_value = sess.run([image,label,imagefile])

Image,label,imagefile 값을 읽은 후에, 확인을 위해서 matplotlib를 이용하여, 이미지와, 라벨, 그리고 파일 경로를 출력하여, 값이 정확하게 읽히는지 순서에 맞게 읽히고 누락은 없는지 확인할수 있다.

(확인을 위해서 데이타를 읽을때 shuffle을 하지 않고 순차적으로 읽었다.)


실행 결과

그 실행 결과를 보면 다음과 같다.



다른 코드


만약에 읽어드린 이미지들을 한꺼번에 보고 싶을 경우에는 아래와 같은 코드를 사용한다. 아래 코드는 200개의 이미지를 읽어서 가로로 10개씩 출력하는 코드이다. 아래 코드 부분을 바꿔치면 된다.

   fig = plt.figure(figsize=(20,120))

   for i in range(200):

       image_value,label_value,imagefile_value = sess.run([image,label,imagefile])

    

       subplot = fig.add_subplot(50,10,i+1)

       subplot.set_xlabel(label_value)

       plt.imshow(image_value)

       print label_value ,imagefile_value

   plt.show(


출력 결과는 다음과 같다.


다음번에는 텐서로 읽어드린 이미지 데이타를 활용하여 얼굴 인식 모델을 CNN으로 만들어보고 학습 시켜 보겠다.




저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

연예인 얼굴 인식 서비스를 만들어보자 #1 - 학습데이타 준비하기


조대협 (http://bcho.tistory.com)


CNN 에 대한 이론 공부와 텐서 플로우에 대한 기본 이해를 끝내서 실제로 모델을 만들어보기로 하였다.

CNN을 이용한 이미지 인식중 대중적인 주제로 얼굴 인식 (Face recognition)을 주제로 잡아서, 이 모델을 만들기로 하고 아직 실력이 미흡하여 호주팀에서 일하고 있는 동료인 Win woo 라는 동료에게 모델과 튜토리얼 개발을 부탁하였다.


이제 부터 연재하는 연예인 얼굴 인식 서비스는 Win woo 가 만든 코드를 기반으로 하여 설명한다. (코드 원본 주소 : https://github.com/wwoo/tf_face )

얼굴 데이타를 내려 받자

먼저 얼굴 인식 모델을 만들려면, 학습을 시킬 충분한 데이타가 있어야 한다. 사람 얼굴을 일일이 구할 수 도 없고, 구글이나 네이버에서 일일이 저장할 수 도 없기 때문에, 공개된 데이타셋을 활용하였는데, PubFig (Public Figures Face Database - http://www.cs.columbia.edu/CAVE/databases/pubfig/) 를 사용하였다.



이 데이타셋에는 약 200명에 대한 58,000여장의 이미지를 저장하고 있는데, 이 중의 일부만을 사용하였다.

Download 페이지로 가면, txt 파일 형태 (http://www.cs.columbia.edu/CAVE/databases/pubfig/download/dev_urls.txt) 로 아래와 같이


Abhishek Bachan 1 http://1.bp.blogspot.com/_Y7rzCyUABeI/SNIltEyEnjI/AAAAAAAABOg/E1keU_52aFc/s400/ash_abhishek_365x470.jpg 183,60,297,174 f533da9fbd1c770428c8961f3fa48950
Abhishek Bachan 2 http://1.bp.blogspot.com/_v9nTKD7D57Q/SQ3HUQHsp_I/AAAAAAAAQuo/DfPcHPX2t_o/s400/normal_14thbombaytimes013.jpg 49,71,143,165 e36a8b24f0761ec75bdc0489d8fd570b
Abhishek Bachan 3 http://2.bp.blogspot.com/_v9nTKD7D57Q/SL5KwcwQlRI/AAAAAAAANxM/mJPzEHPI1rU/s400/ERTYH.jpg 32,68,142,178 583608783525c2ac419b41e538a6925d


사람이름, 이미지 번호, 다운로드 URL, 사진 크기, MD5 체크섬을 이 필드로 저장되어 있다.

이 파일을 이용하여 다운로드 URL에서 사진을 다운받아서, 사람이름으로된 폴더에 저장한다.

물론 수동으로 할 수 없으니 HTTP Client를 이용하여, URL에서 사진을 다운로드 하게 하고, 이를 사람이름 폴더 별로 저장하도록 해야 한다.


HTTP Client를 이용하여 파일을 다운로드 받는 코드는 일반적인 코드이기 때문에 별도로 설명하지 않는다.

본인의 경우에는 Win이 만든 https://github.com/wwoo/tf_face/blob/master/tf/face_extract/pubfig_get.py 코드를 이용하여 데이타를 다운로드 받았다.

사용법은  https://github.com/wwoo/tf_face 에 나와 있는데,


$> python tf/face_extract/pubfig_get.py tf/face_extract/eval_urls.txt ./data

를 실행하면 ./data 디렉토리에 이미지를 다운로드 받아서 사람 이름별 폴더에 저장해준다.

evals_urls.txt에는 위에서 언급한 dev_urls.txt 형태의 데이타가 들어간다.


사람 종류가 너무 많으면 데이타를 정재하는 작업이 어렵고, (왜 어려운지는 뒤에 나옴) 학습 시간이 많이 걸리기 때문에, 약 47명의 데이타를 다운로드 받아서 작업하였다.

쓰레기 데이타 골라내기

데이타를 다운받고 나니, 아뿔사!! PubFig 데이타셋이 오래되어서 없는 이미지도 있고 학습에 적절하지 않은 이미지도 있다.


주로 학습에 적절하지 않은 데이타는 한 사진에 두사람 이상의 얼굴이 있거나, 이미지가 사라져서 위의 우측 그림처럼, 이미지가 없는 형태로 나오는 경우인데, 이러한 데이타는 어쩔 수 없이 눈으로 한장한장 다 걸러내야만 하였다.

아마 이 작업이 가장 오랜 시간이 걸린 작업이 아닐까도 한다. 더불어서 머신러닝이 정교한 수학이나 알고리즘이 아니라 노가다라고 불리는 이유를 알았다.

얼굴 추출하기

다음 학습에 가능한 데이타를 잘 골라내었으면, 학습을 위해서 사진에서 얼굴만을 추출해내야 한다. 포토샵으로 일일이 할 수 없기 때문에 얼굴 영역을 인식하는 API를 사용하기로한다. OPEN CV와 같은 오픈소스 라이브러리를 사용할 수 도 있지만 구글의 VISION API의 경우 얼굴 영역을 아주 잘 잘라내어주고, 코드 수십줄만 가지고도 얼굴 영역을 알아낼 수 있기 때문에 구글 VISION API를 사용하였다.

https://cloud.google.com/vision/




VISION API ENABLE 하기

VISION API를 사용하기 위해서는 해당 구글 클라우드 프로젝트에서 VISION API를 사용하도록 ENABLE 해줘야 한다.

VISION API를 ENABLE하기 위해서는 아래 화면과 같이 구글 클라우드 콘솔 > API Manager 들어간후




+ENABLE API를 클릭하여 아래 그림과 같이 Vision API를 클릭하여 ENABLE 시켜준다.




SERVICE ACCOUNT 키 만들기

다음으로 이 VISION API를 호출하기 위해서는 API 토큰이 필요한데, SERVICE ACCOUNT 라는 JSON 파일을 다운 받아서 사용한다.

구글 클라우드 콘솔에서 API Manager로 들어간후 Credentials 메뉴에서 Create creadential 메뉴를 선택한후, Service account key 메뉴를 선택한다



다음 Create Service Account key를 만들도록 하고, accountname과 id와 같은 정보를 넣는다. 이때 중요한것이 이 키가 가지고 있는 사용자 권한을 설정해야 하는데, 편의상 모든 권한을 가지고 있는  Project Owner 권한으로 키를 생성한다.


(주의. 실제 운영환경에서 전체 권한을 가지는 키는 보안상의 위험하기 때문에 특정 서비스에 대한 접근 권한만을 가지도록 지정하여 Service account를 생성하기를 권장한다.)




Service account key가 생성이 되면, json 파일 형태로 다운로드가 된다.

여기서는 terrycho-ml-80abc460730c.json 이름으로 저장하였다.


예제 코드

그럼 예제를 보자 코드의 전문은 https://github.com/bwcho75/facerecognition/blob/master/com/terry/face/extract/crop_face.py 에 있다.


이 코드는 이미지 파일이 있는 디렉토리를 지정하고, 아웃풋 디렉토리를 지정해주면 이미지 파일을 읽어서 얼굴이 있는지 없는지를 체크하고 얼굴이 있으면, 얼굴 부분만 잘라낸 후에, 얼굴 사진을 96x96 사이즈로 리사즈 한후에,

70%의 파일들은 학습용으로 사용하기 위해서 {아웃풋 디렉토리/training/} 디렉토리에 저장하고

나머지 30%의 파일들은 검증용으로 사용하기 위해서 {아웃풋 디렉토리/validate/} 디렉토리에 저장한다.


그리고 학습용 파일 목록은 다음과 같이 training_file.txt에 파일 위치,사람명(라벨) 형태로 저장하고

/Users/terrycho/traning_datav2/training/wsmith.jpg,Will Smith

/Users/terrycho/traning_datav2/training/wsmith061408.jpg,Will Smith

/Users/terrycho/traning_datav2/training/wsmith1.jpg,Will Smith


검증용 파일들은 validate_file.txt에 마찬가지로  파일위치와, 사람명(라벨)을 저장한다.

사용 방법은 다음과 같다.

python com/terry/face/extract/crop_face.py “원본 파일이있는 디렉토리" “아웃풋 디렉토리"

(원본 파일 디렉토리안에는 {사람이름명} 디렉토리 아래에 사진들이 쭈욱 있는 구조라야 한다.)


자 그러면, 코드의 주요 부분을 살펴보자


VISION API 초기화 하기

  def __init__(self):

       # initialize library

       #credentials = GoogleCredentials.get_application_default()

       scopes = ['https://www.googleapis.com/auth/cloud-platform']

       credentials = ServiceAccountCredentials.from_json_keyfile_name(

                       './terrycho-ml-80abc460730c.json', scopes=scopes)

       self.service = discovery.build('vision', 'v1', credentials=credentials)


초기화 부분은 Google Vision API를 사용하기 위해서 OAuth 인증을 하는 부분이다.

scope를 googleapi로 정해주고, 인증 방식을 Service Account를 사용한다. credentials 부분에 service account key 파일인 terrycho-ml-80abc460730c.json를 지정한다.


얼굴 영역 찾아내기

다음은 이미지에서 얼굴을 인식하고, 얼굴 영역(사각형) 좌표를 리턴하는 함수를 보자


   def detect_face(self,image_file):

       try:

           with io.open(image_file,'rb') as fd:

               image = fd.read()

               batch_request = [{

                       'image':{

                           'content':base64.b64encode(image).decode('utf-8')

                           },

                       'features':[{

                           'type':'FACE_DETECTION',

                           'maxResults':MAX_RESULTS,

                           }]

                       }]

               fd.close()

       

           request = self.service.images().annotate(body={

                           'requests':batch_request, })

           response = request.execute()

           if 'faceAnnotations' not in response['responses'][0]:

                print('[Error] %s: Cannot find face ' % image_file)

                return None

               

           face = response['responses'][0]['faceAnnotations']

           box = face[0]['fdBoundingPoly']['vertices']

           left = box[0]['x']

           top = box[1]['y']

               

           right = box[2]['x']

           bottom = box[2]['y']

               

           rect = [left,top,right,bottom]

               

           print("[Info] %s: Find face from in position %s" % (image_file,rect))

           return rect

       except Exception as e:

           print('[Error] %s: cannot process file : %s' %(image_file,str(e)) )

 

VISION API를 이용하여, 얼굴 영역을 추출하는데, 위의 코드에서 처럼 image_file을 읽은후에, batch_request라는 문자열을 만든다. JSON 형태의 문자열이 되는데, 이때 image라는 항목에 이미지 데이타를 base64 인코딩 방식으로 인코딩해서 전송한다. 그리고 VISION API는 얼굴인식뿐 아니라 사물 인식, 라벨인식등 여러가지 기능이 있기 때문에 그중에서 타입을 ‘FACE_DETECTION’으로 정의하여 얼굴 영역만 인식하도록 한다.


request를 만들었으면, VISION API로 요청을 보내면 응답이 오는데, 이중에서 response 엘리먼트의 첫번째 인자 ( [‘responses’][0] )은 첫번째 얼굴은 뜻하는데, 여기서 [‘faceAnnotation’]을 하면 얼굴에 대한 정보만을 얻을 수 있다. 이중에서  [‘fdBoundingPoly’] 값이 얼굴 영역을 나타내는 사각형이다. 이 갑ㄱㅅ을 읽어서 left,top,right,bottom 값에 세팅한 후 리턴한다.


얼굴 잘라내고 리사이즈 하기

앞의 detect_face에서 찾아낸 얼굴 영역을 가지고 그 부분만 전체 사진에서 잘라내고, 잘라낸 얼굴을 학습에 적합하도록 같은 크기 (96x96)으로 리사이즈 한다.

이런 이미지 처리를 위해서 PIL (Python Imaging Library - http://www.pythonware.com/products/pil/)를 사용하였다.

   def crop_face(self,image_file,rect,outputfile):

       try:

           fd = io.open(image_file,'rb')

           image = Image.open(fd)  

           crop = image.crop(rect)

           im = crop.resize(IMAGE_SIZE,Image.ANTIALIAS)

           im.save(outputfile,"JPEG")

           fd.close()

           print('[Info] %s: Crop face %s and write it to file : %s' %(image_file,rect,outputfile) )

       except Exception as e:

           print('[Error] %s: Crop image writing error : %s' %(image_file,str(e)) )

image_file을 인자로 받아서 , rect 에 정의된 사각형 영역 만큼 crop를 해서 잘라내고, resize 함수를 이용하여 크기를 96x96으로 조정한후 (참고 IMAGE_SIZE = 96,96 로 정의되어 있다.) outputfile 경로에 저장하게 된다.        


실행을 해서 정재된 데이타는 다음과 같다.


생각해볼만한점들

이 코드는 간단한 토이 프로그램이기 때문에 간단하게 작성했지만 실제 운영환경에 적용하기 위해서는 몇가지 고려해야 할 사항이 있다.

먼저, 이 코드는 싱글 쓰레드로 돌기 때문에 속도가 상대적으로 느리다 그래서 멀티 쓰레드로 코드를 수정할 필요가 있으며, 만약에 수백만장의 사진을 정재하기 위해서는 한대의 서버로 되지 않기 때문에, 원본 데이타를 여러 서버로 나눠서 처리할 수 있는 분산 처리 구조가 고려되어야 한다.

또한, VISION API로 사진을 전송할때는 BASE64 인코딩된 구조로 서버에 이미지를 직접 전송하기 때문에, 자칫 이미지 사이즈들이 크면 네트워크 대역폭을 많이 잡아먹을 수 있기 때문에 가능하다면 식별이 가능한 크기에서 리사이즈를 한 후에, 서버로 전송하는 것이 좋다. 실제로 필요한 얼굴 크기는 96x96 픽셀이기 때문에 필요없이 1000만화소 고화질의 사진들을 전송해서 네트워크 비용을 낭비하지 않기를 바란다.


다음은 이렇게 정재한 파일들을 텐서플로우에서 읽어서 학습 데이타로 활용하는 방법에 대해서 알아보겠다.


저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

텐서플로우 - 파일에서 학습데이타를 읽어보자#2


CSV 파일을 읽어보자

조대협 (http://bcho.tistory.com)


이 글은 http://bcho.tistory.com/1163 의 두번째 글이다. 앞의 글을 먼저 읽고 읽기를 권장한다.

앞의 글에서는 트레이닝 파일명의 목록을 읽어서 큐에 넣고, 파일명을 하나씩 읽어오는 처리 방법에 대해서 알아보았다. 이번 글에서는 그 파일들에 있는 데이타를 읽어서 파싱한 후, 실제 트레이닝 세션에 학습용 데이타로 불러들이는 방법을 설명하도록 한다.

파일에서 데이타 읽기 (Reader)

finename_queue에 파일명이 저장되었으면, 이 파일들을 하나씩 읽어서 처리하는 방법을 알아본다.

파일에서 데이타를 읽어오는 컴포넌트를 Reader라고 한다. 이 Reader들은 filename_queue에 저장된 파일들을 하나씩 읽어서, 그 안에 있는 데이타를 읽어서 리턴한다.


예를 들어 TextLineReader의 경우에는 , 텍스트 파일에서, 한줄씩 읽어서 문자열을 리턴한다.


꼭 텐서플로우에서 미리 정해져있는 Reader 들을 사용할 필요는 없지만, 미리 정의된 Reader를 쓰면 조금 더 편리하다.

미리 정의된 Reader로는 Text File에서, 각 필드가 일정한 길이를 가지고 있을때 사용할 수 있는, FixedLengthRecordReader 그리고, 텐서플로우 데이타를 바이너리 포맷으로 저장하는 TFRecord 포맷에 대한 리더인 TFRecordReader 등이 있다.


Reader를 사용하는 방법은 다음과 같다.

reader = tf.TextLineReader()

key,value = reader.read(filename_queue)


먼저 Reader 변수를 지정한 다음, reader.read를 이용하여 filename_queue 로 부터 파일을 읽게 하면 value에 파일에서 읽은 값이 리턴이 된다

예를 들어 csv 파일에 아래와 같은 문자열이 들어가 있다고 할때


167c9599-c97d-4d42-bdb1-027ddaed07c0,1,2016,REG,3:54

67ea7e52-333e-43f3-a668-6d7893baa8fb,1,2016,REG,2:11

9e44593b-a870-446e-aed5-90a22ab0c952,1,2016,REG,2:32

48832a52-e56c-467f-a1ef-c6f8c6e908ea,1,2016,REG,2:17


위의 코드 처럼, TextLineReader를 이용하여 파일을 읽게 되면 value에는

처음에는 “167c9599-c97d-4d42-bdb1-027ddaed07c0,1,2016,REG,3:54”이, 다음에는 “67ea7e52-333e-43f3-a668-6d7893baa8fb,1,2016,REG,2:11” 문자열이 순차적으로 리턴된다.

읽은 데이타를 디코딩 하기 (Decoder)

Reader에서 읽은 값은 파일의 원시 데이타 (raw)데이타이다. 아직 파싱(해석)이 된 데이타가 아닌데,

예를 들어 Reader를 이용해서 csv 파일을 읽었을 때, Reader에서 리턴되는 값은 csv 파일의 각 줄인 문자열이지, csv 파일의 각 필드 데이타가 아니다.


즉 우리가 학습에서 사용할 데이타는

167c9599-c97d-4d42-bdb1-027ddaed07c0,1,2016,REG,3:54

하나의 문자열이 아니라

Id = “167c9599-c97d-4d42-bdb1-027ddaed07c0”,

Num  = 1

Year = 2016

rType = “REG”

rTime = “3:54”

과 같이 문자열이 파싱된 각 필드의 값이 필요하다.


이렇게 읽어드린 데이타를 파싱 (해석) 하는 컴포넌트를 Decoder라고 한다.


Reader와 마찬가지로, Decoder 역시 미리 정해진 Decoder 타입이 있는데, JSON,CSV 등 여러가지 데이타 포맷에 대한 Decoder를 지원한다.

위의 CSV 문자열을 csv 디코더를 이용하여 파싱해보자


record_defaults = [ ["null"],[1],[1900],["null"],["null"]]

id, num, year, rtype , rtime = tf.decode_csv(

   value, record_defaults=record_defaults,field_delim=',')


csv decoder를 사용하기 위해서는 각 필드의 디폴트 값을 지정해줘야 한다. record_default는 각 필드의 디폴트 값을 지정해 주는 것은 물론이고, 각 필드의 데이타 타입을 (string,int,float etc)를 정의 하는 역할을 한다.

디폴트 값은 csv 데이타에서 해당 필드가 비워져 있을때 채워 진다.

위에서는 record_deafult에서 첫번째 필드는 string 형이고 디폴트는 “null”로, 두번째 필드는 integer 형이고, 디폴트 값은 1로, 세번째 필드는 integer 형이고 디폴트는 1900 으로, 네번째와 다섯번째 필드는 모두 string형이고, 디폴트 값을 “null” 로 지정하였다.

이 디폴트 값 세팅을 가지고 tf.decode_csv를 이용하여 파싱 한다.

value는 앞에서 읽어 드린 CSV 문자열이다. record_defaults= 를 이용하여 레코드의 형과 디폴트 값을 record_defaults에 정해진 값으로 지정하였고, CSV 파일에서 각 필드를 구분하기 위한 구분자를 ‘,’를 사용한다는 것을 명시 하였다.

다음 Session을 실행하여, 이 Decoder를 실행하면 csv의 각 행을 파싱하여, 각 필드를 id,num,year,rtype,rtime이라는 필드에 리턴하게 된다.


이를 정리해보면 다음과 같은 구조를 가지게 된다.


예제

위에서 설명한 CSV 파일명을 받아서 TextLineReader를 이용하여 각 파일을 읽고, 각 파일에서 CSV 포맷의 데이타를 읽어서 출력하는 예제의 전체 코드를 보면 다음과 같다.


import tensorflow as tf

from numpy.random.mtrand import shuffle


#define filename queue

filename_queue = tf.train.string_input_producer(['/Users/terrycho/training_datav2/queue_test_data/b1.csv'

                                                ,'/Users/terrycho/training_datav2/queue_test_data/c2.csv']

                                                ,shuffle=False,name='filename_queue')

# define reader

reader = tf.TextLineReader()

key,value = reader.read(filename_queue)


#define decoder

record_defaults = [ ["null"],[1],[1900],["null"],["null"]]

id, num, year, rtype , rtime = tf.decode_csv(

   value, record_defaults=record_defaults,field_delim=',')


with tf.Session() as sess:

   

   coord = tf.train.Coordinator()

   threads = tf.train.start_queue_runners(sess=sess, coord=coord)

   

   for i in range(100):

       print(sess.run([id, num, year, rtype , rtime]))

   

   coord.request_stop()

   coord.join(threads)                                        


지금까지 파일에서 데이타를 읽어서 학습 데이타로 사용하는 방법에 대해서 알아보았다.

다음에는 이미지 기반의 CNN 모델을 학습 시키기 위해서 이미지 데이타를 전처리 하고 읽는 방법에 대해서 설명하도록 하겠다.

저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

텐서보드를 이용하여 학습 과정을 시각화 해보자


조대협 (http://bcho.tistory.com)


텐서플로우로 머신러닝 모델을 만들어서 학습해보면, 각 인자에 어떤 값들이 학습이 진행되면서 어떻게 변화하는지 모니터링 하기가 어렵다. 앞의 예제들에서는 보통 콘솔에 텍스트로 loss 값이나, accuracy 값을 찍어서, 학습 상황을 봤는데, 텐서보다는 학습에 사용되는 각종 지표들이 어떻게 변화하는지 손쉽게 시각화를 해준다.


예를 들어 보면 다음 그림은 학습을 할때 마다 loss 값이 어떻게 변하는지를 보여주는 그래프이다.

가로축은 학습 횟수를 세로축은 모델의 loss 값을 나타낸다.





잘 보면 두개의 그래프가 그려져 있는 것을 볼 수 있는데, 1st 그래프는 첫번째 학습, 2nd 는 두번째 학습에서  추출한 loss 값이다.

Visualize Learning

그러면 어떻게 학습 과정을 시각화할 수 있는지를 알아보자

학습 과정을 시각화 하려면 학습중에 시각화 하려는 데이타를 tf.summary 모듈을 이용해서 중간중간에 파일로 기록해놨다가, 학습이 끝난 후에 이 파일을 텐서 보드를 통해서 읽어서 시각화 한다. 이를 위해서 다음과 같이 크게 4가지 메서드가 주로 사용이 된다.

  • tf.summary.merge_all
    Summary를 사용하기 위해서 초기화 한다.

  • tf.summary.scalar(name,value)
    Summary에 추가할 텐서를 정의 한다. name에는 이름, vallue에는 텐서를 정의한다. Scalar 형 텐서로 (즉 다차원 행렬이 아닌, 단일 값을 가지는 텐서형만 사용이 가능하다.) 주로 accuracy나 loss와 같은 스칼라형 텐서에 사용한다.

  • tf.summary.histogram(name,value)
    값(value) 에 대한 분포도를 보고자 할때 사용한다. .scalar와는 다르게 다차원 텐서를 사용할 수 있다. 입력 데이타에 대한 분포도나, Weight, Bias값의 변화를 모니터링할 수 있다.

  • tf.train.SummaryWriter
    파일에 summary 데이타를 쓸때 사용한다.


예제는 https://www.tensorflow.org/tutorials/mnist/tf/ 를 참고하면 된다.


mnist.py에서 아래와 같이 loss 값을 모니터링 하기 위해서 tf.summary.scalar를 이용하여 ‘loss’라는 이름으로 loss 텐서를 모니터링하기 위해서 추가하였다.


다음 fully_connected_feed.py에서

Summary를 초기화 하고, 세션이 시작된 후에, summary_writer를 아래와 같이 초기화 하였다.


이때, 파일 경로 (FLAGS.log_dir)을 설정하고, 텐서 플로우의 세션 그래프(sess.graph)를 인자로 넘긴다.




다음 트레이닝 과정에서, 100번마다, summary 값을 문자열로 변환하여, summary_writer를 이용하여 파일에 저장하였다.


트레이닝이 끝나면 위에서 지정된 디렉토리에 아래와 같이 summary 데이타 파일이 생성 된다.



이를 시각화 하려면 콘솔에서 tensorboard --logdir=”Summary 파일 디렉토리 경로" 를 지정해주면 6060 포트로 텐서보드 웹 사이트가 준비된다.



웹 브라우져를 열어서 localhost:6060에 접속해보면 다음과 같은 그림이 나온다.


Loss 값이 트레이닝이 수행됨에 따라 작아 지는 것을 볼 수 있다. (총 2000번 트레이닝을 하였다.)

세로축은 loss 값, 가로축은 학습 스텝이 된다.


만약에 여러번 학습을 하면서 모델을 튜닝했다면, 각 학습 별로 loss 값이나 accuracy 값이 어떻게 변하는지 그래프를 중첩하여 비교하고 싶을 수 있는데, 이 경우에는


% tensorboard --logdir=이름1:로그경로2,이름2:로그경로2,....


이런식으로 “이름:로그경로"를 ,로 구분하여 여러개를 써주면 그래프를 중첩하여 볼 수 있다.

아래는 1st, 2nd 두개의 이름으로 두개의 summary 로그를 중첩하여 시각화하여 각 학습 별로 loss 값이 어떻게 변화 하는지를 보여주는 그래프 이다.



Histogram

히스토 그램은 다차원 텐서에 대한 분포를 볼 수 있는 방법인데,

https://github.com/llSourcell/Tensorboard_demo 에 히스토그램을 텐서보드로 모니터링할 수 있는 좋은 샘플이 있다. 이 코드는 세개의 히든레이어를 갖는 뉴럴네트워크인데, (사실 좀 코드는 이상하다. Bias 값도 더하지 않았고, 일반 레이어 없이 dropout 레이어만 엮었다. 모델 자체가 맞는지 틀리는지는 따지지 말고 어떻게 Histogram을 모니터링 하는지를 살펴보자)


모델 그래프는 다음과 같다.




다음, 각 레이어에서 사용된 weight 값인 w_h,w_h2,w_o를 모니터링 하기 위해서 이 텐서들을 tf.historgram_summary를 이용하여 summary에 저장 한다.



이렇게 저장된 데이타를 텐서 보드로 시각화 해보면


Distribution 탭에서는 다음과 같은 값을 볼 수 있다.



w_h_summ 값의 분포인데, 세로 축은 w의 값, 가로축은 학습 횟수 이다.

학습이 시작되는 초기에는 w값이 0을 중심으로 좌우 대칭으로 모여 있는 것을 볼 수 있다. 잘 보면, 선이 있는 것을 볼 수 있는데, 색이 진할 수 록, 값이 많이 모여 있는 것이고 흐릴 수 록 값이 적게 있는 것이다.


다른 뷰로는 Histogram View를 보면, 다음과 같은 그래프를 볼 수 있는데,



세로축이 학습 횟수, 가로축이 Weight의 값이다.

그래프가 여러개가 중첩 되어 있는 것을 볼 수 있는데, 각각의 그래프는 각 학습시에 나온 Weight의 값으로, 위의 그래프에서 보면 중앙에 값이 집중되어 있다가, 아래 그래프를 보면 값이 점차적으로 옆으로 퍼지는 것을 볼 수 있다.


사실 개인적인 의견이지만 Weight 값의 분포를 보는 것이 무슨 의미를 가지는지는 잘 모르겠다. CNN에서 필터링 된 피쳐의 분포나, 또는 원본 데이타의 분포에는 의미가 있을듯하다.


저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

딥러닝을 이용한 숫자 이미지 인식 #2/2


앞서 MNIST 데이타를 이용한 필기체 숫자를 인식하는 모델을 컨볼루셔널 네트워크 (CNN)을 이용하여 만들었다. 이번에는 이 모델을 이용해서 필기체 숫자 이미지를 인식하는 코드를 만들어 보자


조금 더 테스트를 쉽게 하기 위해서, 파이썬 주피터 노트북내에서 HTML 을 이용하여 마우스로 숫자를 그릴 수 있도록 하고, 그려진 이미지를 어떤 숫자인지 인식하도록 만들어 보겠다.



모델 로딩

먼저 앞의 예제에서 학습을한 모델을 로딩해보도록 하자.

이 코드는 주피터 노트북에서 작성할때, 모델을 학습 시키는 코드 (http://bcho.tistory.com/1156) 와 별도의 새노트북에서 구현을 하도록 한다.


코드

import tensorflow as tf

import numpy as np

import matplotlib.pyplot as plt

from tensorflow.examples.tutorials.mnist import input_data


#이미 그래프가 있을 경우 중복이 될 수 있기 때문에, 기존 그래프를 모두 리셋한다.

tf.reset_default_graph()


num_filters1 = 32


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

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


#  layer 1

W_conv1 = tf.Variable(tf.truncated_normal([5,5,1,num_filters1],

                                         stddev=0.1))

h_conv1 = tf.nn.conv2d(x_image, W_conv1,

                      strides=[1,1,1,1], padding='SAME')


b_conv1 = tf.Variable(tf.constant(0.1, shape=[num_filters1]))

h_conv1_cutoff = tf.nn.relu(h_conv1 + b_conv1)


h_pool1 =tf.nn.max_pool(h_conv1_cutoff, ksize=[1,2,2,1],

                       strides=[1,2,2,1], padding='SAME')


num_filters2 = 64


# layer 2

W_conv2 = tf.Variable(

           tf.truncated_normal([5,5,num_filters1,num_filters2],

                               stddev=0.1))

h_conv2 = tf.nn.conv2d(h_pool1, W_conv2,

                      strides=[1,1,1,1], padding='SAME')


b_conv2 = tf.Variable(tf.constant(0.1, shape=[num_filters2]))

h_conv2_cutoff = tf.nn.relu(h_conv2 + b_conv2)


h_pool2 =tf.nn.max_pool(h_conv2_cutoff, ksize=[1,2,2,1],

                       strides=[1,2,2,1], padding='SAME')


# fully connected layer

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*num_filters2])


num_units1 = 7*7*num_filters2

num_units2 = 1024


w2 = tf.Variable(tf.truncated_normal([num_units1, num_units2]))

b2 = tf.Variable(tf.constant(0.1, shape=[num_units2]))

hidden2 = tf.nn.relu(tf.matmul(h_pool2_flat, w2) + b2)


keep_prob = tf.placeholder(tf.float32)

hidden2_drop = tf.nn.dropout(hidden2, keep_prob)


w0 = tf.Variable(tf.zeros([num_units2, 10]))

b0 = tf.Variable(tf.zeros([10]))

k = tf.matmul(hidden2_drop, w0) + b0

p = tf.nn.softmax(k)


# prepare session

sess = tf.InteractiveSession()

sess.run(tf.global_variables_initializer())

saver = tf.train.Saver()

saver.restore(sess, '/Users/terrycho/anaconda/work/cnn_session')


print 'reload has been done'


그래프 구현

코드를 살펴보면, #prepare session 부분 전까지는 이전 코드에서의 그래프를 정의하는 부분과 동일하다. 이 코드는 우리가 만든 컨볼루셔널 네트워크를 복원하는 부분이다.


변수 데이타 로딩

그래프의 복원이 끝나면, 저장한 세션의 값을 다시 로딩해서 학습된 W와 b값들을 다시 로딩한다.


# prepare session

sess = tf.InteractiveSession()

sess.run(tf.global_variables_initializer())

saver = tf.train.Saver()

saver.restore(sess, '/Users/terrycho/anaconda/work/cnn_session')


이때 saver.restore 부분에서 앞의 예제에서 저장한 세션의 이름을 지정해준다.

HTML을 이용한 숫자 입력

그래프와 모델 복원이 끝났으면 이 모델을 이용하여, 숫자를 인식해본다.

테스트하기 편리하게 HTML로 마우스로 숫자를 그릴 수 있는 화면을 만들어보겠다.

주피터 노트북에서 새로운 Cell에 아래와 같은 내용을 입력한다.


코드

input_form = """

<table>

<td style="border-style: none;">

<div style="border: solid 2px #666; width: 143px; height: 144px;">

<canvas width="140" height="140"></canvas>

</div></td>

<td style="border-style: none;">

<button onclick="clear_value()">Clear</button>

</td>

</table>

"""


javascript = """

<script type="text/Javascript">

   var pixels = [];

   for (var i = 0; i < 28*28; i++) pixels[i] = 0

   var click = 0;


   var canvas = document.querySelector("canvas");

   canvas.addEventListener("mousemove", function(e){

       if (e.buttons == 1) {

           click = 1;

           canvas.getContext("2d").fillStyle = "rgb(0,0,0)";

           canvas.getContext("2d").fillRect(e.offsetX, e.offsetY, 8, 8);

           x = Math.floor(e.offsetY * 0.2)

           y = Math.floor(e.offsetX * 0.2) + 1

           for (var dy = 0; dy < 2; dy++){

               for (var dx = 0; dx < 2; dx++){

                   if ((x + dx < 28) && (y + dy < 28)){

                       pixels[(y+dy)+(x+dx)*28] = 1

                   }

               }

           }

       } else {

           if (click == 1) set_value()

           click = 0;

       }

   });

   

   function set_value(){

       var result = ""

       for (var i = 0; i < 28*28; i++) result += pixels[i] + ","

       var kernel = IPython.notebook.kernel;

       kernel.execute("image = [" + result + "]");

   }

   

   function clear_value(){

       canvas.getContext("2d").fillStyle = "rgb(255,255,255)";

       canvas.getContext("2d").fillRect(0, 0, 140, 140);

       for (var i = 0; i < 28*28; i++) pixels[i] = 0

   }

</script>

"""


다음 새로운 셀에서, 다음 코드를 입력하여, 앞서 코딩한 HTML 파일을 실행할 수 있도록 한다.


from IPython.display import HTML

HTML(input_form + javascript)


이제 앞에서 만든 두 셀을 실행시켜 보면 다음과 같이 HTML 기반으로 마우스를 이용하여 숫자를 입력할 수 있는 박스가 나오는것을 확인할 수 있다.



입력값 판정

앞의 HTML에서 그린 이미지는 앞의 코드의 set_value라는 함수에 의해서, image 라는 변수로 784 크기의 벡터에 저장된다. 이 값을 이용하여, 이 그림이 어떤 숫자인지를 앞서 만든 모델을 이용해서 예측을 해본다.


코드


p_val = sess.run(p, feed_dict={x:[image], keep_prob:1.0})


fig = plt.figure(figsize=(4,2))

pred = p_val[0]

subplot = fig.add_subplot(1,1,1)

subplot.set_xticks(range(10))

subplot.set_xlim(-0.5,9.5)

subplot.set_ylim(0,1)

subplot.bar(range(10), pred, align='center')

plt.show()

예측

예측을 하는 방법은 쉽다. 이미지 데이타가 image 라는 변수에 들어가 있기 때문에, 어떤 숫자인지에 대한 확률을 나타내는 p 의 값을 구하면 된다.


p_val = sess.run(p, feed_dict={x:[image], keep_prob:1.0})


를 이용하여 x에 image를 넣고, 그리고 dropout 비율을 0%로 하기 위해서 keep_prob를 1.0 (100%)로 한다. (예측이기 때문에 당연히 dropout은 필요하지 않다.)

이렇게 하면 이 이미지가 어떤 숫자인지에 대한 확률이 p에 저장된다.

그래프로 표현

그러면 이 p의 값을 찍어 보자


fig = plt.figure(figsize=(4,2))

pred = p_val[0]

subplot = fig.add_subplot(1,1,1)

subplot.set_xticks(range(10))

subplot.set_xlim(-0.5,9.5)

subplot.set_ylim(0,1)

subplot.bar(range(10), pred, align='center')

plt.show()


그래프를 이용하여 0~9 까지의 숫자 (가로축)일 확률을 0.0~1.0 까지 (세로축)으로 출력하게 된다.

다음은 위에서 입력한 숫자 “4”를 인식한 결과이다.



(보너스) 첫번째 컨볼루셔널 계층 결과 출력

컨볼루셔널 네트워크를 학습시키다 보면 종종 컨볼루셔널 계층을 통과하여 추출된 특징 이미지들이 어떤 모양을 가지고 있는지를 확인하고 싶을때가 있다. 그래서 각 필터를 통과한 값을 이미지로 출력하여 확인하고는 하는데, 여기서는 이렇게 각 필터를 통과하여 인식된 특징이 어떤 모양인지를 출력하는 방법을 소개한다.


아래는 우리가 만든 네트워크 중에서 첫번째 컨볼루셔널 필터를 통과한 결과 h_conv1과, 그리고 이 결과에 bias 값을 더하고 활성화 함수인 Relu를 적용한 결과를 출력하는 예제이다.


코드


conv1_vals, cutoff1_vals = sess.run(

   [h_conv1, h_conv1_cutoff], feed_dict={x:[image], keep_prob:1.0})


fig = plt.figure(figsize=(16,4))


for f in range(num_filters1):

   subplot = fig.add_subplot(4, 16, f+1)

   subplot.set_xticks([])

   subplot.set_yticks([])

   subplot.imshow(conv1_vals[0,:,:,f],

                  cmap=plt.cm.gray_r, interpolation='nearest')

plt.show()


x에 image를 입력하고, dropout을 없이 모든 네트워크를 통과하도록 keep_prob:1.0으로 주고, 첫번째 컨볼루셔널 필터를 통과한 값 h_conv1 과, 이 값에 bias와 Relu를 적용한 값 h_conv1_cutoff를 계산하였다.

conv1_vals, cutoff1_vals = sess.run(

   [h_conv1, h_conv1_cutoff], feed_dict={x:[image], keep_prob:1.0})


첫번째 필터는 총 32개로 구성되어 있기 때문에, 32개의 결과값을 imshow 함수를 이용하여 흑백으로 출력하였다.




다음은 bias와 Relu를 통과한 값인 h_conv_cutoff를 출력하는 예제이다. 위의 코드와 동일하며 subplot.imgshow에서 전달해주는 인자만 conv1_vals → cutoff1_vals로 변경되었다.


코드


fig = plt.figure(figsize=(16,4))


for f in range(num_filters1):

   subplot = fig.add_subplot(4, 16, f+1)

   subplot.set_xticks([])

   subplot.set_yticks([])

   subplot.imshow(cutoff1_vals[0,:,:,f],

                  cmap=plt.cm.gray_r, interpolation='nearest')

   

plt.show()


출력 결과는 다음과 같다



이제까지 컨볼루셔널 네트워크를 이용한 이미지 인식을 텐서플로우로 구현하는 방법을 MNIST(필기체 숫자 데이타)를 이용하여 구현하였다.


실제로 이미지를 인식하려면 전체적인 흐름은 같지만, 이미지를 전/후처리 해내야 하고 또한 한대의 머신이 아닌 여러대의 머신과 GPU와 같은 하드웨어 장비를 사용한다. 다음 글에서는 MNIST가 아니라 실제 칼라 이미지를 인식하는 방법에 대해서 데이타 전처리에서 부터 서비스까지 전체 과정에 대해서 설명하도록 하겠다.


예제 코드 : https://github.com/bwcho75/tensorflowML/blob/master/MNIST_CNN_Prediction.ipynb


저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

딥러닝을 이용한 숫자 이미지 인식 #1/2


조대협 (http://bcho.tistory.com)


지난 글(http://bcho.tistory.com/1154 ) 을 통해서 소프트맥스 회귀를 통해서, 숫자를 인식하는 모델을 만들어서 학습 시켜 봤다.

이번글에서는 소프트맥스보다 정확성이 높은 컨볼루셔널 네트워크를 이용해서 숫자 이미지를 인식하는 모델을 만들어 보겠다.


이 글의 목적은 CNN 자체의 설명이나, 수학적 이론에 대한 이해가 목적이 아니다. 최소한의 수학적 지식만 가지고, CNN 네트워크 모델을 텐서플로우로 구현하는데에 그 목적을 둔다. CNN을 이해하기 위해서는 Softmax 등의 함수를 이해하는게 좋기 때문에 가급적이면 http://bcho.tistory.com/1154 예제를 먼저 보고 이 문서를 보는게 좋다. 그 다음에 CNN 모델에 대한 개념적인 이해를 위해서 http://bcho.tistory.com/1149  문서를 참고하고 이 문서를 보는 것이 좋다.


이번 글은 CNN을 적용하는 것 이외에, 다음과 같은 몇가지 팁을 추가로 소개한다.

  • 학습이 된 모델을 저장하고 다시 로딩 하는 방법

  • 학습된 모델을 이용하여 실제로 주피터 노트북에서 글씨를 써보고 인식하는 방법

MNIST CNN 모델


우리가 만들고자 하는 모델은 두개의 컨볼루셔널 레이어(Convolutional layer)과, 마지막에 풀리 커넥티드 레이어 (fully connected layer)을 가지고 있는 컨볼루셔널 네트워크 모델(CNN) 이다.

모델의 모양을 그려보면 다음과 같다.


입력 데이타

입력으로 사용되는 데이타는 앞의 소프트맥스 예제에서 사용한 데이타와 동일한 손으로 쓴 숫자들이다. 각 숫자 이미지는 28x28 픽셀로 되어 있고, 흑백이미지이기 때문에 데이타는 28x28x1 행렬이 된다. (만약에 칼라 RGB라면 28x28x3이 된다.)

컨볼루셔널 계층

총 두 개의 컨볼루셔널 계층을 사용했으며, 각 계층에서 컨볼루셔널 필터를 사용해서, 특징을 추출한다음에, 액티베이션 함수 (Activation function)으로, ReLu를 적용한 후, 맥스풀링 (Max Pooling)을 이용하여, 주요 특징을 정리해낸다.

이와 같은 컨볼루셔널 필터를 두개를 중첩하여 적용하였다.

마지막 풀리 커넥티드 계층

컨볼루셔널 필터를 통해서 추출된 특징은 풀리 커넥티드 레이어(Fully connected layer)에 의해서 분류 되는데, 풀리 커넥티드 레이어는 하나의 뉴럴 네트워크를 사용하고, 그 뒤에 드롭아웃 (Dropout) 계층을 넣어서, 오버피팅(Overfitting)이 발생하는 것을 방지한다.  마지막으로 소프트맥스 (Softmax) 함수를 이용하여 0~9 열개의 숫자로 분류를 한다.


학습(트레이닝) 코드

이를 구현하기 위한 코드는 다음과 같다.


코드

import tensorflow as tf

import numpy as np

import matplotlib.pyplot as plt

from tensorflow.examples.tutorials.mnist import input_data



tf.reset_default_graph()


np.random.seed(20160704)

tf.set_random_seed(20160704)


# load data

mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)


# define first layer

num_filters1 = 32


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

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


W_conv1 = tf.Variable(tf.truncated_normal([5,5,1,num_filters1],

                                         stddev=0.1))

h_conv1 = tf.nn.conv2d(x_image, W_conv1,

                      strides=[1,1,1,1], padding='SAME')


b_conv1 = tf.Variable(tf.constant(0.1, shape=[num_filters1]))

h_conv1_cutoff = tf.nn.relu(h_conv1 + b_conv1)


h_pool1 = tf.nn.max_pool(h_conv1_cutoff, ksize=[1,2,2,1],

                        strides=[1,2,2,1], padding='SAME')


# define second layer

num_filters2 = 64


W_conv2 = tf.Variable(

           tf.truncated_normal([5,5,num_filters1,num_filters2],

                               stddev=0.1))

h_conv2 = tf.nn.conv2d(h_pool1, W_conv2,

                      strides=[1,1,1,1], padding='SAME')


b_conv2 = tf.Variable(tf.constant(0.1, shape=[num_filters2]))

h_conv2_cutoff = tf.nn.relu(h_conv2 + b_conv2)


h_pool2 = tf.nn.max_pool(h_conv2_cutoff, ksize=[1,2,2,1],

                        strides=[1,2,2,1], padding='SAME')


# define fully connected layer

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*num_filters2])


num_units1 = 7*7*num_filters2

num_units2 = 1024


w2 = tf.Variable(tf.truncated_normal([num_units1, num_units2]))

b2 = tf.Variable(tf.constant(0.1, shape=[num_units2]))

hidden2 = tf.nn.relu(tf.matmul(h_pool2_flat, w2) + b2)


keep_prob = tf.placeholder(tf.float32)

hidden2_drop = tf.nn.dropout(hidden2, keep_prob)


w0 = tf.Variable(tf.zeros([num_units2, 10]))

b0 = tf.Variable(tf.zeros([10]))

k = tf.matmul(hidden2_drop, w0) + b0

p = tf.nn.softmax(k)


#define loss (cost) function

t = tf.placeholder(tf.float32, [None, 10])

loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(k,t))

train_step = tf.train.AdamOptimizer(0.0001).minimize(loss)

correct_prediction = tf.equal(tf.argmax(p, 1), tf.argmax(t, 1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))


# prepare session

sess = tf.InteractiveSession()

sess.run(tf.global_variables_initializer())

saver = tf.train.Saver()


# start training

i = 0

for _ in range(1000):

   i += 1

   batch_xs, batch_ts = mnist.train.next_batch(50)

   sess.run(train_step,

            feed_dict={x:batch_xs, t:batch_ts, keep_prob:0.5})

   if i % 500 == 0:

       loss_vals, acc_vals = [], []

       for c in range(4):

           start = len(mnist.test.labels) / 4 * c

           end = len(mnist.test.labels) / 4 * (c+1)

           loss_val, acc_val = sess.run([loss, accuracy],

               feed_dict={x:mnist.test.images[start:end],

                          t:mnist.test.labels[start:end],

                          keep_prob:1.0})

           loss_vals.append(loss_val)

           acc_vals.append(acc_val)

       loss_val = np.sum(loss_vals)

       acc_val = np.mean(acc_vals)

       print ('Step: %d, Loss: %f, Accuracy: %f'

              % (i, loss_val, acc_val))


saver.save(sess, 'cnn_session')

sess.close()



데이타 로딩 파트

그러면 코드를 하나씩 살펴보도록 하자.

맨 처음 블럭은 데이타를 로딩하고 각종 변수를 초기화 하는 부분이다.

import tensorflow as tf

import numpy as np

import matplotlib.pyplot as plt

from tensorflow.examples.tutorials.mnist import input_data


#Call tf.reset_default_graph() before you build your model (and the Saver). This will ensure that the variables get the names you intended, but it will invalidate previously-created graphs.


tf.reset_default_graph()


np.random.seed(20160704)

tf.set_random_seed(20160704)


# load data

mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)


Input_data 는 텐서플로우에 내장되어 있는 MNIST (손으로 쓴 숫자 데이타)셋으로, read_data_sets 메서드를 이요하여 데이타를 읽었다. 데이타 로딩 부분은 앞의 소프트맥스 MNIST와 같으니 참고하기 바란다.


여기서 특히 주목해야 할 부분은 tf.reset_default_graph()  인데, 주피터 노트북과 같은 환경에서 실행을 하게 되면, 주피터 커널을 리스타트하지 않는 이상 변수들의 컨택스트가 그대로 유지 되기 때문에, 위의 코드를 같은 커널에서 tf.reset_default_graph() 없이, 두 번 이상 실행하게 되면 에러가 난다. 그 이유는 텐서플로우 그래프를 만들어놓고, 그 그래프가 지워지지 않은 상태에서 다시 같은 그래프를 생성하면서 나오는 에러인데, tf.reset_default_graph() 메서드는 기존에 생성된 디폴트 그래프를 모두 삭제해서 그래프가 중복되는 것을 막아준다. 일반적인 파이썬 코드에서는 크게 문제가 없지만, 컨택스트가 계속 유지되는 주피터 노트북 같은 경우에는 발생할 수 있는 문제이니, 반드시 디폴트 그래프를 리셋해주도록 하자

첫번째 컨볼루셔널 계층

필터의 정의

다음은 첫번째 컨볼루셔널 계층을 정의 한다. 컨볼루셔널 계층을 이해하려면 컨볼루셔널 필터에 대한 개념을 이해해야 하는데, 다시 한번 되짚어 보자.

컨볼루셔널 계층에서 하는 일은 입력 데이타에 필터를 적용하여, 특징을 추출해 낸다.


이 예제에서 입력 받는 이미지 데이타는  28x28x1 행렬로 표현된 흑백 숫자 이미지이고, 예제 코드에서는 5x5x1 사이즈의 필터를 적용한다.

5x5x1 사이즈의 필터 32개를 적용하여, 총 32개의 특징을 추출할것이다.


코드

필터 정의 부분까지 코드로 살펴보면 다음과 같다.

# define first layer

num_filters1 = 32


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

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


W_conv1 = tf.Variable(tf.truncated_normal([5,5,1,num_filters1],


x는 입력되는 이미지 데이타로, 2차원 행렬(28x28)이 아니라, 1차원 벡터(784)로 되어 있고, 데이타의 수는 무제한으로 정의하지 않았다. 그래서 placeholder정의에서 shape이 [None,784] 로 정의 되어 있다.  

예제에서는 연산을 편하게 하기 위해서 2차원 행렬을 사용할것이기 때문에, 784 1차원 벡터를 28x28x1 행렬로 변환을 해준다.

x_image는 784x무한개인 이미지 데이타 x를 , (28x28x1)이미지의 무한개 행렬로  reshape를 이용하여 변경하였다. [-1,28,28,1]은 28x28x1 행렬을 무한개(-1)로 정의하였다.


필터를 정의하는데, 필터는 앞서 설명한것과 같이 5x5x1 필터를 사용할것이고, 필터의 수는 32개이기 때문에, 필터 W_conv1의 차원(shape)은 [5,5,1,32] 가된다. (코드에서 32는 num_filters1 이라는 변수에 저장하여 사용하였다.) 그리고 W_conv1의 초기값은 [5,5,1,32] 차원을 가지는 난수를 생성하도록 tf.truncated_normal을 사용해서 임의의 수가 지정되도록 하였다.

필터 적용

필터를 정의했으면 필터를 입력 데이타(이미지)에 적용한다.


h_conv1 = tf.nn.conv2d(x_image, W_conv1,

                      strides=[1,1,1,1], padding='SAME')


필터를 적용하는 방법은 tf.nn.conv2d를 이용하면 되는데, 28x28x1 사이즈의 입력 데이타인 x_image에 앞에서 정의한 필터 W_conv1을 적용하였다.

스트라이드 (Strides)

필터는 이미지의 좌측 상단 부터 아래 그림과 같이 일정한 간격으로 이동하면서 적용된다.


이를 개념적으로 표현하면 다음과 같은 모양이 된다.


이렇게 필터를 움직이는 간격을 스트라이드 (Stride)라고 한다.

예제에서는 우측으로 한칸 그리고 끝까지 이동하면 아래로 한칸을 이동하도록 각각 가로와 세로의 스트라이드 값을 1로 세팅하였다.

코드에서 보면

h_conv1 = tf.nn.conv2d(x_image, W_conv1,

                      strides=[1,1,1,1], padding='SAME')

에서 strides=[1,1,1,1] 로 정의한것을 볼 수 있다. 맨앞과 맨뒤는 통상적으로 1을 쓰고, 두번째 1은 가로 스트라이드 값, 그리고 세번째 1은 세로 스트라이드 값이 된다.

패딩 (Padding)

위의 그림과 같이 필터를 적용하여 추출된 특징 행렬은 원래 입력된 이미지 보다 작게 된다.

연속해서 필터를 이런 방식으로 적용하다 보면 필터링 된 특징들이  작아지게되는데, 만약에 특징을  다 추출하기 전에 특징들이 의도하지 않게 유실되는 것을 막기 위해서 패딩이라는 것을 사용한다.


패딩이란, 입력된 데이타 행렬 주위로, 무의미한 값을 감싸서 원본 데이타의 크기를 크게 해서, 필터를 거치고 나온 특징 행렬의 크기가 작아지는 것을 방지한다.

또한 무의미한 값을 넣음으로써, 오버피팅이 발생하는 것을 방지할 수 있다. 코드상에서 padding 변수를 이용하여 패딩 방법을 정의하였다.


h_conv1 = tf.nn.conv2d(x_image, W_conv1,

                      strides=[1,1,1,1], padding='SAME')



padding=’SAME’을 주게 되면, 텐서플로우가 자동으로 패딩을 삽입하여 입력값과 출력값 (특징 행렬)의 크기가 같도록 한다. padding=’VALID’를 주게 되면, 패딩을 적용하지 않고 필터를 적용하여 출력값 (특징 행렬)의 크기가 작아진다.

활성함수 (Activation function)의 적용

필터 적용이 끝났으면, 이 필터링된 값에 활성함수를 적용한다. 컨볼루셔널 네트워크에서 일반적으로 사용하는 활성함수는 ReLu 함수이다.


코드

b_conv1 = tf.Variable(tf.constant(0.1, shape=[num_filters1]))

h_conv1_cutoff = tf.nn.relu(h_conv1 + b_conv1)


먼저 bias 값( y=WX+b 에서 b)인 b_conv1을 정의하고, tf.nn.relu를 이용하여, 필터된 결과(h_conv1)에 bias 값을 더한 값을 ReLu 함수로 적용하였다.

Max Pooling

추출된 특징 모두를 가지고 특징을 판단할 필요가 없이, 일부 특징만을 가지고도 특징을 판단할 수 있다. 즉 예를 들어서 고해상도의 큰 사진을 가지고도 어떤 물체를 식별할 수 있지만, 작은 사진을 가지고도 물체를 식별할 수 있다. 이렇게 특징의 수를 줄이는 방법을 서브샘플링 (sub sampling)이라고 하는데, 서브샘플링을 해서 전체 특징의 수를 의도적으로 줄이는 이유는 데이타의 크기를 줄이기 때문에, 컴퓨팅 파워를 절약할 수 있고, 데이타가 줄어드는 과정에서 데이타가 유실이 되기 때문에, 오버 피팅을 방지할 수 있다.


이러한 서브 샘플링에는 여러가지 방법이 있지만 예제에서는 맥스 풀링 (max pooling)이라는 방법을 사용했는데, 맥스 풀링은 풀링 사이즈 (mxn)로 입력데이타를 나눈후 그 중에서 가장 큰 값만을 대표값으로 추출하는 것이다.


아래 그림을 보면 원본 데이타에서 2x2 사이즈로 맥스 풀링을 해서 결과를 각 셀별로 최대값을 뽑아내었고, 이 셀을 가로 2칸씩 그리고 그다음에는 세로로 2칸씩 이동하는 stride 값을 적용하였다.


코드

h_pool1 = tf.nn.max_pool(h_conv1_cutoff, ksize=[1,2,2,1],

                        strides=[1,2,2,1], padding='SAME')


Max pooling은 tf.nn.max_pool이라는 함수를 이용해서 적용할 수 있는데, 첫번째 인자는 활성화 함수 ReLu를 적용하고 나온 결과 값인 h_conv1_cutoff 이고, 두 번째 인자인 ksize는 풀링 필터의 사이즈로 [1,2,2,1]은 2x2 크기로 묶어서 풀링을 한다는 의미이다.


다음 stride는 컨볼루셔널 필터 적용과 마찬가지로 풀링 필터를 가로와 세로로 얼마만큼씩 움직일 것인데, strides=[1,2,2,1]로, 가로로 2칸, 세로로 2칸씩 움직이도록 정의하였다.


행렬의 차원 변환

텐서플로우를 이용해서 CNN을 만들때 각각 개별의 알고리즘을 이해할 필요는 없지만 각 계층을 추가하거나 연결하기 위해서는 행렬의 차원이 어떻게 바뀌는지는 이해해야 한다.

다음 그림을 보자


첫번째 컨볼루셔널 계층은 위의 그림과 같이, 처음에 28x28x1 의 이미지가 들어가면 32개의 컨볼루셔널 필터 W를 적용하게 되고, 각각은 28x28x1의 결과 행렬을 만들어낸다. 컨볼루셔널 필터를 거치게 되면 결과 행렬의 크기는 작아져야 정상이지만, 결과 행렬의 크기를 입력 행렬의 크기와 동일하게 유지하도록 padding=’SAME’으로 설정하였다.

다음으로 bias 값 b를 더한후 (위의 그림에는 생략하였다) 에 이 값에 액티베이션 함수 ReLu를 적용하고 나면 행렬 크기에 변화 없이 28x28x1 행렬 32개가 나온다. 이 각각의 행렬에 size가 2x2이고, stride가 2인 맥스풀링 필터를 적용하게 되면 각각의 행렬의 크기가 반으로 줄어들어 14x14x1 행렬 32개가 리턴된다.


두번째 컨볼루셔널 계층


이제 두번째 컨볼루셔널 계층을 살펴보자. 첫번째 컨볼루셔널 계층과 다를 것이 없다.


코드

# define second layer

num_filters2 = 64


W_conv2 = tf.Variable(

           tf.truncated_normal([5,5,num_filters1,num_filters2],

                               stddev=0.1))

h_conv2 = tf.nn.conv2d(h_pool1, W_conv2,

                      strides=[1,1,1,1], padding='SAME')


b_conv2 = tf.Variable(tf.constant(0.1, shape=[num_filters2]))

h_conv2_cutoff = tf.nn.relu(h_conv2 + b_conv2)


h_pool2 = tf.nn.max_pool(h_conv2_cutoff, ksize=[1,2,2,1],

                        strides=[1,2,2,1], padding='SAME')


단 필터값인 W_conv2의 차원이 [5,5,32,64] ([5,5,num_filters1,num_filters2] 부분 )로 변경되었다.


W_conv2 = tf.Variable(

           tf.truncated_normal([5,5,num_filters1,num_filters2],

                               stddev=0.1))


필터의 사이즈가 5x5이고, 입력되는 값이 32개이기 때문에, 32가 들어가고, 총 64개의 필터를 적용하기 때문에 마지막 부분이 64가 된다.

첫번째 필터와 똑같이 stride를 1,1을 줘서 가로,세로로 각각 1씩 움직이고, padding=’SAME’으로 입력과 출력 사이즈를 같게 하였다.


h_pool2 = tf.nn.max_pool(h_conv2_cutoff, ksize=[1,2,2,1],

                        strides=[1,2,2,1], padding='SAME')


맥스풀링 역시 첫번째 필터와 마찬가지로 2,2 사이즈의 필터(ksize=[1,2,2,1]) 를 적용하고 stride값을 2,2로 줘서 (strides=[1,2,2,1]) 가로 세로로 두칸씩 움직이게 하여 결과의 크기가 반으로 줄어들게 하였다.


14x14 크기의 입력값 32개가 들어가서, 7x7 크기의 행렬 64개가 리턴된다.

풀리 커넥티드 계층

두개의 컨볼루셔널 계층을 통해서 특징을 뽑아냈으면, 이 특징을 가지고 입력된 이미지가 0~9 중 어느 숫자인지를 풀리 커넥티드 계층 (Fully connected layer)를 통해서 판단한다.


코드

# define fully connected layer

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*num_filters2])


num_units1 = 7*7*num_filters2

num_units2 = 1024


w2 = tf.Variable(tf.truncated_normal([num_units1, num_units2]))

b2 = tf.Variable(tf.constant(0.1, shape=[num_units2]))

hidden2 = tf.nn.relu(tf.matmul(h_pool2_flat, w2) + b2)


keep_prob = tf.placeholder(tf.float32)

hidden2_drop = tf.nn.dropout(hidden2, keep_prob)


w0 = tf.Variable(tf.zeros([num_units2, 10]))

b0 = tf.Variable(tf.zeros([10]))

k = tf.matmul(hidden2_drop, w0) + b0

p = tf.nn.softmax(k)


입력된 64개의 7x7 행렬을 1차원 행렬로 변환한다.


h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*num_filters2])


다음으로 풀리 커넥티드 레이어에 넣는데, 이때 입력값은 64x7x7 개의 벡터 값을 1024개의 뉴런을 이용하여 학습한다.


w2 = tf.Variable(tf.truncated_normal([num_units1, num_units2]))

b2 = tf.Variable(tf.constant(0.1, shape=[num_units2]))


그래서 w2의 값은 [num_units1,num_units2]로 num_units1은 64x7x7 로 입력값의 수를, num_unit2는 뉴런의 수를 나타낸다. 다음 아래와 같이 이 뉴런으로 계산을 한 후 액티베이션 함수 ReLu를 적용한다.


hidden2 = tf.nn.relu(tf.matmul(h_pool2_flat, w2) + b2)


다음 레이어에서는 드롭 아웃을 정의하는데, 드롭 아웃은 오버피팅(과적합)을 막기 위한 계층으로, 원리는 다음 그림과 같이 몇몇 노드간의 연결을 끊어서 학습된 데이타가 도달하지 않도록 하여서 오버피팅이 발생하는 것을 방지하는 기법이다.


출처 : http://cs231n.github.io/neural-networks-2/


텐서 플로우에서 드롭 아웃을 적용하는 것은 매우 간단하다. 아래 코드와 같이 tf.nn.dropout 이라는 함수를 이용하여, 앞의 네트워크에서 전달된 값 (hidden2)를 넣고 keep_prob에, 연결 비율을 넣으면 된다.

keep_prob = tf.placeholder(tf.float32)

hidden2_drop = tf.nn.dropout(hidden2, keep_prob)


연결 비율이란 네트워크가 전체가 다 연결되어 있으면 1.0, 만약에 50%를 드롭아웃 시키면 0.5 식으로 입력한다.

드롭 아웃이 끝난후에는 결과를 가지고 소프트맥스 함수를 이용하여 10개의 카테고리로 분류한다.


w0 = tf.Variable(tf.zeros([num_units2, 10]))

b0 = tf.Variable(tf.zeros([10]))

k = tf.matmul(hidden2_drop, w0) + b0

p = tf.nn.softmax(k)

비용 함수 정의

여기까지 모델 정의가 끝났다. 이제 이 모델을 학습 시키기 위해서 비용함수(코스트 함수)를 정의해보자.

코스트 함수는 크로스엔트로피 함수를 이용한다.

#define loss (cost) function

t = tf.placeholder(tf.float32, [None, 10])

loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(k,t))

train_step = tf.train.AdamOptimizer(0.0001).minimize(loss)


k는 앞의 모델에 의해서 앞의 모델에서

k = tf.matmul(hidden2_drop, w0) + b0

p = tf.nn.softmax(k)


으로 softmax를 적용하기 전의 값이다.  Tf.nn.softmax_cross_entropy_with_logits 는 softmax가 포함되어 있는 함수이기 때문에, p를 적용하게 되면 softmax 함수가 중첩 적용되기 때문에, softmax 적용전의 값인 k 를 넣었다.


WARNING: This op expects unscaled logits, since it performs a softmax on logits internally for efficiency. Do not call this op with the output of softmax, as it will produce incorrect results

https://github.com/tensorflow/tensorflow/blob/master/tensorflow/g3doc/api_docs/python/functions_and_classes/shard7/tf.nn.softmax_cross_entropy_with_logits.md


t는 플레이스 홀더로 정의하였는데, 나중에 학습 데이타 셋에서 읽을 라벨 (그 그림이 0..9 중 어느 숫자인지)이다.


그리고 이 비용 함수를 최적화 하기 위해서 최적화 함수 AdamOptimizer를 사용하였다.

(앞의 소프트맥스 예제에서는 GradientOptimizer를 사용하였는데, 일반적으로 AdamOptimizer가 좀 더 무난하다.)

학습

이제 모델 정의와, 모델의 비용함수와 최적화 함수까지 다 정의하였다. 그러면 이 그래프들을 데이타를 넣어서 학습 시켜보자.  학습은 배치 트레이닝을 이용할것이다.


학습 도중 학습의 진행상황을 보기 위해서 학습된 모델을 중간중간 테스트할것이다. 테스트할때마다 학습의 정확도를 측정하여 출력하는데, 이를 위해서 정확도를 계산하는 함수를 아래와 같이 정의한다.


#define validation function

correct_prediction = tf.equal(tf.argmax(p, 1), tf.argmax(t, 1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))


correct_prediction은 학습 결과와 입력된 라벨(정답)을 비교하여 맞았는지 틀렸는지를 리턴한다.

argmax는 인자에서 가장 큰 값의 인덱스를 리턴하는데, 0~9 배열이 들어가 있기 때문에 가장 큰 값이 학습에 의해 예측된 숫자이다. p는 예측에 의한 결과 값이고, t는 라벨 값이다 이 두 값을 비교하여 가장 큰 값이 있는 인덱스가 일치하면 예측이 성공한것이다.

correct_pediction은 bool 값이기 때문에, 이 값을 숫자로 바꾸기 위해서 tf.reduce_mean을 사용하여, accuracy에 저장하였다.


이제 학습을 세션을 시작하고, 변수들을 초기화 한다.

# prepare session

sess = tf.InteractiveSession()

sess.run(tf.global_variables_initializer())

saver = tf.train.Saver()


다음 배치 학습을 시작한다.

# start training

i = 0

for _ in range(10000):

   i += 1

   batch_xs, batch_ts = mnist.train.next_batch(50)

   sess.run(train_step,

            feed_dict={x:batch_xs, t:batch_ts, keep_prob:0.5})

   if i % 500 == 0:

       loss_vals, acc_vals = [], []

       for c in range(4):

           start = len(mnist.test.labels) / 4 * c

           end = len(mnist.test.labels) / 4 * (c+1)

           loss_val, acc_val = sess.run([loss, accuracy],

               feed_dict={x:mnist.test.images[start:end],

                          t:mnist.test.labels[start:end],

                          keep_prob:1.0})

           loss_vals.append(loss_val)

           acc_vals.append(acc_val)

       loss_val = np.sum(loss_vals)

       acc_val = np.mean(acc_vals)

       print ('Step: %d, Loss: %f, Accuracy: %f'

              % (i, loss_val, acc_val))


학습은 10,000번 루프를 돌면서 한번에 50개씩 배치로 데이타를 읽어서 학습을 진행하고, 500 번째 마다 중각 학습 결과를 출력한다. 중간 학습 결과에서는 10,000 중 몇번째 학습인지와, 비용값 그리고 정확도를 출력해준다.


코드를 보자


   batch_xs, batch_ts = mnist.train.next_batch(50)


MNIST 학습용 데이타 셋에서 50개 단위로 데이타를 읽는다. batch_xs에는 학습에 사용할 28x28x1 사이즈의 이미지와, batch_ts에는 그 이미지에 대한 라벨 (0..9중 어떤 수인지) 가 들어 있다.

읽은 데이타를 feed_dict를 통해서 피딩(입력)하고 트레이닝 세션을 시작한다.


  sess.run(train_step,

            feed_dict={x:batch_xs, t:batch_ts, keep_prob:0.5})


이때 마지막 인자에 keep_prob를 0.5로 피딩하는 것을 볼 수 있는데, keep_prob는 앞의 드롭아웃 계층에서 정의한 변수로 드롭아웃을 거치지 않을 비율을 정의한다. 여기서는 0.5 즉 50%의 네트워크를 인위적으로 끊도록 하였다.


배치로 학습을 진행하다가 500번 마다 중간중간 정확도와 학습 비용을 계산하여 출력한다.

   if i % 500 == 0:

       loss_vals, acc_vals = [], []


여기서 주목할 점은 아래 코드 처럼 한번에 검증을 하지 않고 테스트 데이타를 4등분 한후, 1/4씩 테스트 데이타를 로딩해서 학습비용(loss)와 학습 정확도(accuracy)를 계산하는 것을 볼 수 있다.


       for c in range(4):

           start = len(mnist.test.labels) / 4 * c

           end = len(mnist.test.labels) / 4 * (c+1)

           loss_val, acc_val = sess.run([loss, accuracy],

               feed_dict={x:mnist.test.images[start:end],

                          t:mnist.test.labels[start:end],

                          keep_prob:1.0})

           loss_vals.append(loss_val)

           acc_vals.append(acc_val)


이유는 한꺼번에 많은 데이타를 로딩해서 검증을 할 경우 메모리 문제가 생길 수 있기 때문에, 4번에 나눠 걸쳐서 읽고 검증한 다음에 아래와 같이 학습 비용은 4번의 학습 비용을 합하고, 정확도는 4번의 학습 정확도를 평균으로 내어 출력하였다.


       loss_val = np.sum(loss_vals)

       acc_val = np.mean(acc_vals)

       print ('Step: %d, Loss: %f, Accuracy: %f'

              % (i, loss_val, acc_val))

학습 결과 저장

학습을 통해서 최적의 W와 b값을 구했으면 이 값을 예측에 이용해야 하는데, W 값들이 많고, 이를 일일이 출력해서 파일로 저장하는 것도 번거롭고 해서, 텐서플로우에서는 학습된 모델을 저장할 수 있는 기능을 제공한다. 학습을 통해서 계산된 모든 변수 값을 저장할 수 있는데,  앞에서 세션을 생성할때 생성한 Saver (saver = tf.train.Saver())를 이용하면 현재 학습 세션을  저장할 수 있다.


코드

saver.save(sess, 'cnn_session')

sess.close()


이렇게 하면 현재 디렉토리에 cnn_session* 형태의 파일로 학습된 세션 값들이 저장된다.

그래서 추후 예측을 할때 다시 학습할 필요 없이 이 파일을 로딩해서, 모델의 값들을 복귀한 후에, 예측을 할 수 있다. 이 파일을 읽어서 예측을 하는 것은 다음글에서 다루기로 한다.


예제 코드 : https://github.com/bwcho75/tensorflowML/blob/master/MNIST_CNN_Training.ipynb


저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

딥러닝 - 컨볼루셔널 네트워크를 이용한 이미지 인식의 개념


조대협 (http://bcho.tistory.com)


이번 글에서는 딥러닝 중에서 이미지 인식에 많이 사용되는 컨볼루셔널 뉴럴 네트워크 (Convolutional neural network) 이하 CNN에 대해서 알아보도록 하자.


이 글을 읽기에 앞서서 머신러닝에 대한 기본 개념이 없는 경우는 다음 글들을 참고하기 바란다.



CNN은 전통적인 뉴럴 네트워크 앞에 여러 계층의 컨볼루셔널 계층을 붙인 모양이 되는데, 그 이유는 다음과 같다. CNN은 앞의 컨볼루셔널 계층을 통해서 입력 받은 이미지에 대한 특징(Feature)를 추출하게 되고, 이렇게 추출된 특징을 기반으로 기존의 뉴럴 네트워크를 이용하여 분류를 해내게 된다.




컨볼루셔널 레이어  (Convolutional Layer)

컨볼루셔널 레이어는 앞에서 설명 했듯이 입력 데이타로 부터 특징을 추출하는 역할을 한다.

컨볼루셔널 레이어는 특징을 추출하는 기능을 하는 필터(Filter)와, 이 필터의 값을 비선형 값으로 바꾸어 주는 액티베이션 함수(Activiation 함수)로 이루어진다.

그럼 각 부분의 개념과 원리에 대해서 살펴보도록 하자.


<그림 Filter와 Activation 함수로 이루어진 Convolutional 계층>

필터 (Filter)

필터 개념 이해

필터는 그 특징이 데이타에 있는지 없는지를 검출해주는 함수이다. 예를 들어 아래와 같이 곡선을 검출해주는 필터가 있다고 하자.



필터는 구현에서는 위의 그림 좌측 처럼 행렬로 정의가 된다.

입력 받은 이미지 역시 행렬로 변환이 되는데, 아래 그림을 보자.


쥐 그림에서 좌측 상단의 이미지 부분을 잘라내서 필터를 적용하는 결과이다.

잘라낸 이미지와, 필터를 곱하면

과 같이 결과 값이 매우 큰 값이 나온다.

만약에 아래 그림처럼 쥐 그림에서 곡선이 없는 부분에 같은 필터를 적용해보면


결과 값이 0에 수렴하게 나온다.


즉 필터는 입력받은 데이타에서 그 특성을 가지고 있으면 결과 값이 큰값이 나오고, 특성을 가지고 있지 않으면 결과 값이 0에 가까운 값이 나오게 되서 데이타가 그 특성을 가지고 있는지 없는지 여부를 알 수 있게 해준다.

다중 필터의 적용

입력값에는 여러가지 특징이 있기 때문에 하나의 필터가 아닌 여러개의 다중 필터를 같이 적용하게 된다.

다음과 같이 |,+,- 모양을 가지고 있는 데이타가 있다고 하자


각 데이타가 |와 - 의 패턴(특징을) 가지고 있는지를 파악하기 위해서 먼저 | (세로) 필터를 적용해보면 다음과 같은 결과가 나온다.


(맨앞의 상자는 필터이다.) 두번째 상자부터 원본 이미지에 세로선(|) 이 없는 경우 결과 이미지에 출력이 없고, 세로선이 있는 경우에는 결과 이미지에 세로 선이 있는 것을 확인할 수 있다.

마찬가지로 가로선(-) 특징이 있는지 가로 선을 추출하는 필터를 적용해보면 다음과 같은 결과를 얻을 수 있다.



이렇게 각기 다른 특징을 추출하는 필터를 조합하여 네트워크에 적용하면, 원본 데이타가 어떤 형태의 특징을 가지고 있는지 없는지를 판단해 낼 수 있다. 다음은 하나의 입력 데이타에 앞서 적용한 세로와 가로선에 대한 필터를 동시에 적용한 네트워크의 모양이다.



Stride

그러면 이 필터를 어떻게 원본 이미지에 적용할까? 큰 사진 전체에 하나의 큰 필터 하나만을 적용할까?

아래 그림을 보자, 5x5 원본 이미지가 있을때, 3x3인 필터를 좌측 상단에서 부터 왼쪽으로 한칸씩 그 다음 한줄을 내려서 또 왼쪽으로 한칸씩 적용해서 특징을 추출해낸다.

오른쪽 Convolved Feature 행렬이 바로 원본 이미지에 3x3 필터를 적용하여 얻어낸 결과 이다.



이렇게 필터를 적용 하는 간격 (여기서는 우측으로 한칸씩 그리고 아래로 한칸씩 적용하였다.) 값을 Stride라고 하고, 필터를 적용해서 얻어낸 결과를 Feature map 또는 activation map 이라고 한다.

Padding

앞에서 원본 데이타에 필터를 적용한 내용을 보면 필터를 적용한 후의 결과값은 필터 적용전 보다 작아졌다. 5x5 원본 이미지가 3x3의 1 stride 값을 가지고 적용되었을때, 결과 값은 3x3으로 크기가 작아졌다.

그런데, CNN 네트워크는 하나의 필터 레이어가 아니라 여러 단계에 걸쳐서 계속 필터를 연속적으로 적용하여 특징을 추출하는 것을 최적화 해나가는데, 필터 적용 후 결과 값이 작아지게 되면 처음에 비해서 특징이 많이 유실 될 수 가 있다. 필터를 거쳐감에 따라서 특징이 유실되는 것을 기대했다면 문제가 없겠지만, 아직까지 충분히 특징이 추출되기 전에, 결과 값이 작아지면 특징이 유실된다. 이를 방지 하기 위한 방법으로 padding 이라는 기법이 있는데, padding은 결과 값이 작아지는 것을 방지하기 위해서 입력값 주위로 0 값을 넣어서 입력 값의 크기를 인위적으로 키워서, 결과값이 작아지는 것을 방지 하는 기법이다.


다음 그림을 보자, 32x32x3 입력값이 있을때, 5x5x3 필터를 적용 시키면 결과값 (feature map)의 크기는 28x28x3 이 된다. 이렇게 사이즈가 작아지는 것을 원하지 않았다면 padding을 적용하는데, input 계층 주위로 0을 둘러 싸서, 결과 값이 작아지고 (피쳐가 소실 되는것)을 막는다

32x32x3 입력값 주위로 2 두깨로 0을 둘러싸주면 36x36x3 이 되고 5x5x3 필터 적용하더라도, 결과값 은 32x32x3으로 유지된다.


< 그림, 32x32x3 데이타에 폭이 2인 padding을 적용한 예 >


패딩은 결과 값을 작아지는 것을 막아서 특징이 유실되는 것을 막는 것 뿐 아니라, 오버피팅도 방지하게 되는데, 원본 데이타에 0 값을 넣어서 원래의 특징을 희석 시켜 버리고, 이것을 기반으로 머신러닝 모델이 트레이닝 값에만 정확하게 맞아 들어가는 오버피팅 현상을 방지한다.


오버 피팅에 대해서는 별도의 다른 글을 통해서 설명한다.

필터는 어떻게 만드는 것일까?

그렇다면 CNN에서 사용되는 이런 필터는 어떻게 만드는 것일까? CNN의 신박한 기능이 바로 여기에 있는데, 이 필터는 데이타를 넣고 학습을 시키면, 자동으로 학습 데이타에서 학습을 통해서 특징을 인식하고 필터를 만들어 낸다.

Activation function

필터들을 통해서 Feature map이 추출되었으면, 이 Feature map에 Activation function을 적용하게 된다.

Activation function의 개념을 설명하면, 위의 쥐 그림에서 곡선값의 특징이 들어가 있는지 안들어가 있는지의 필터를 통해서 추출한 값이 들어가 있는 예에서는 6000, 안 들어가 있는 예에서는 0 으로 나왔다.

이 값이 정량적인 값으로 나오기 때문에, 그 특징이 “있다 없다”의 비선형 값으로 바꿔 주는 과정이 필요한데, 이 것이 바로 Activation 함수이다.


예전에 로지스틱 회귀 ( http://bcho.tistory.com/1142 )에서 설명하였던 시그모이드(sigmoid) 함수가 이 Activation 함수에 해당한다.

간단하게 짚고 넘어가면, 결과 값을 참/거짓 으로 나타내는 것이 아니라, 참에 가까워면 0.5~1사이에서 1에 가까운 값을 거짓에 가까우면 0~0.5 사이의 값으로 리턴하는 것이다.


<그림. Sigmoid 함수>

뉴럴 네트워크나 CNN (CNN도 뉴럴 네트워크이다.) 이 Activation 함수로 이 sigmoid 함수는 잘 사용하지 않고, 아래 그림과 같은 ReLu 함수를 주요 사용한다.




<그림. ReLu 함수>

이 함수를 이용하는 이유는 뉴럴 네트워크에서 신경망이 깊어질 수 록 학습이 어렵기 때문에, 전체 레이어를 한번 계산한 후, 그 계산 값을 재 활용하여 다시 계산하는 Back propagation이라는 방법을 사용하는데, sigmoid 함수를 activation 함수로 사용할 경우, 레이어가 깊어지면 이 Back propagation이 제대로 작동을 하지 않기 때문에,(값을 뒤에서 앞으로 전달할때 희석이 되는 현상. 이를 Gradient Vanishing 이라고 한다.) ReLu라는 함수를 사용한다.

풀링 (Sub sampling or Pooling)

이렇게 컨볼루셔날 레이어를 거쳐서 추출된 특징들은 필요에 따라서 서브 샘플링 (sub sampling)이라는 과정을 거친다.


컨볼루셔널 계층을 통해서 어느정도 특징이 추출 되었으면, 이 모든 특징을 가지고 판단을 할 필요가 없다.

쉽게 예를 들면, 우리가 고해상도 사진을 보고 물체를 판별할 수 있지만, 작은 사진을 가지고도 그 사진의 내용이 어떤 사진인지 판단할 수 있는 원리이다.


그래서, 추출된 Activation map을 인위로 줄이는 작업을 하는데, 이 작업을 sub sampling 도는 pooling 이라고 한다. Sub sampling은 여러가지 방법이 있는데, max pooling, average pooling, L2-norm pooling 등이 있고, 그중에서 max pooling 이라는 기법이 많이 사용된다.


Max pooling (맥스 풀링)

맥스 풀링은 Activation map을 MxN의 크기로 잘라낸 후, 그 안에서 가장 큰 값을 뽑아내는 방법이다.

아래 그림을 보면 4x4 Activation map에서 2x2 맥스 풀링 필터를 stride를 2로 하여 2칸씩 이동하면서 맥스 풀링을 한 예인데, 좌측 상단에서는 6이 가장 큰 값이기 때문에 6을 뽑아내고, 우측 상단에는 2,4,7,8 중 8 이 가장 크기 때문에 8을 뽑아 내었다.


맥스 풀링은 특징의 값이 큰 값이 다른 특징들을 대표한다는 개념을 기반으로 하고 있다.

(주의 풀링은 액티베이션 함수 마다 매번 적용하는 것이 아니라, 데이타의 크기를 줄이고 싶을때 선택적으로 사용하는 것이다.)


이런 sampling 을 통해서 얻을 수 있는 장점은 다음과 같다.

  • 전체 데이타의 사이즈가 줄어들기 때문에 연산에 들어가는 컴퓨팅 리소스가 적어지고

  • 데이타의 크기를 줄이면서 소실이 발생하기 때문에, 오버피팅을 방지할 수 있다.


컨볼루셔널 레이어

이렇게 컨볼루셔널 필터와 액티베이션 함수 (ReLU) 그리고 풀링 레이어를 반복적으로 조합하여 특징을 추출한다.

아래 그림을 보면 여러개의 컨볼루셔널 필터(CONV)와 액티베이션 함수 (RELU)와 풀링 (POOL) 사용된것을 볼 수 있다.


Fully connected Layer

컨볼루셔널 계층에서 특징이 추출이 되었으면 이 추출된 특징 값을 기존의 뉴럴 네트워크 (인공 신경 지능망)에 넣어서 분류를 한다.

그래서 CNN의 최종 네트워크 모양은 다음과 같이 된다.



<그림. CNN 네트워크의 모양>

Softmax 함수

Fully connected network (일반적인 뉴럴 네트워크)에 대해서는 이미 알고 있겠지만, 위의 그림에서 Softmax 함수가 가장 마지막에 표현되었기 때문에, 다시 한번 짚고 넘어가자.

Softmax도 앞에서 언급한 sigmoid나 ReLu와 같은 액티베이션 함수의 일종이다.


Sigmoid 함수가 이산 분류 (결과값에 따라 참 또는 거짓을 나타내는) 함수라면, Softmax 는 여러개의 분류를 가질 수 있는 함수이다. 아래 그림이 Softmax 함수의 그림이다.




이것이 의미하는 바는 다음과 같다. P3(x)는 특징(feature) x에 대해서 P3일 확률, P1(x)는 특징 x 에 대해서 P1인 확률이다.

Pn 값은 항상 0~1.0의 범위를 가지며,  P1+P2+...+Pn = 1이 된다.


예를 들어서 사람을 넣었을때, 설현일 확률 0.9, 지현인 확율 0.1 식으로 표시가 되는 것이다.

Dropout 계층

위 CNN 그래프에서 특이한 점중 하나는 Fully connected 네트워크와 Softmax 함수 중간에 Dropout layer (드롭아웃) 라는 계층이 있는 것을 볼 수 있다.


드롭 아웃은 오버피팅(over-fit)을 막기 위한 방법으로 뉴럴 네트워크가 학습중일때, 랜덤하게 뉴런을 꺼서 학습을 방해함으로써, 학습이 학습용 데이타에 치우치는 현상을 막아준다.



<그림. 드롭 아웃을 적용한 네트워크 >

그림 출처 : https://leonardoaraujosantos.gitbooks.io/artificial-inteligence/content/dropout_layer.html


일반적으로 CNN에서는 이 드롭아웃 레이어를 Fully connected network 뒤에 놓지만, 상황에 따라서는 max pooling 계층 뒤에 놓기도 한다.


다음은 드롭아웃을 적용하고 학습시킨 모델과 드롭 아웃을 적용하지 않은 모델 사이의 예측 정확도를 비교한 결과 이다.



<그림. 드룹아웃을 적용한 경우와 적용하지 않고 학습한 경우, 에러율의 차이 >

이렇게 복잡한데 어떻게 구현을 하나요?

대략적인 개념은 이해를 했다. 그렇다면 구현을 어떻게 해야 할까? 앞에서 설명을 할때, softmax 나 뉴런에 대한 세부 알고리즘 ReLu 등과 같은 알고리즘에 대한 수학적인 공식을 설명하지 않았다. 그렇다면 이걸 하나하나 공부해야 할까?


아니다. 작년에 구글에서 머신러닝용 프로그래밍 프레임워크로 텐서 플로우라는 것을 발표했다.

이 텐서 플로우는 (http://www.tensorflow.org)는 이런 머신 러닝에 특화된 프레임웍으로, 머신러닝에 필요한 대부분의 함수들을 이미 구현하여 제공한다.

실제로 CNN을 구현한 코드를 보자. 이 코드는 홍콩 과학기술 대학교의 김성훈 교수님의 강의를 김성훈님이란 분이 텐서 플로우 코드로 구현하여 공유해놓은 코드중 CNN 구현 예제이다. https://github.com/FuZer/Study_TensorFlow/blob/master/08%20-%20CNN/CNN.py




첫번째 줄을 보면, tf.nn.conv2d 라는 함수를 사용하였는데, 이 함수는 컨볼루셔널 필터를 적용한 함수 이다. 처음 X는 입력값이며, 두번째 w 값은 필터 값을 각각 행렬로 정의 한다. 그 다음 strides 값을 정의해주고, 마지막으로 padding 인자를 통해서  padding 사이즈를 정한다.

컨볼루셔널 필터를 적용한 후 액티베이션 함수로 tf.nn.relu를 이용하여 ReLu 함수를 적용한 것을 볼 수 있다.

다음으로는 tf.nn.max_pool 함수를 이용하여, max pooling을 적용하고 마지막으로 tf.nn.dropout 함수를 이용하여 dropout을 적용하였다.


전문적인 수학 지식이 없이도, 이미 잘 추상화된 텐서플로우 함수를 이용하면, 기본적인 개념만 가지고도 머신러닝 알고리즘 구현이 가능하다.


텐서 플로우를 공부하는 방법은 여러가지가 있겠지만, 유투브에서 이찬우님이 강의 하고 계신 텐서 플로우 강의를 듣는 것을 추천한다. 한글이고 설명이 매우 쉽다. 그리고 매주 일요일에 생방송을 하는데, 궁금한것도 물어볼 수 있다.

https://www.youtube.com/channel/UCRyIQSBvSybbaNY_JCyg_vA


그리고 텐서플로우 사이트의 튜토리얼도 상당히 잘되어 있는데, https://www.tensorflow.org/versions/r0.12/tutorials/index.html 를 보면 되고 한글화도 잘 진행되고 있다. 한글화된 문서는 https://tensorflowkorea.gitbooks.io/tensorflow-kr/content/ 에서 찾을 수 있다.

구현은 할 수 있겠는데, 그러면 이 모델은 어떻게 만드나요?

그럼 텐서플로우를 이용하여 모델을 구현할 수 있다는 것은 알았는데, 그렇다면 모델은 어떻게 만들까? 정확도를 높이려면 수십 계층의 뉴럴 네트워크를 설계해야 하고, max pooling  함수의 위치와 padding등 여러가지를 고려해야 하는데, 과연 이게 가능할까?


물론 전문적인 지식을 가진 데이타 과학자라면 이런 모델을 직접 설계하고 구현하고 테스트 하는게 맞겠지만, 이런 모델들은 이미 다양한 모델이 만들어져서 공개 되어 있다.


그중에서 CNN 모델은 매년 이미지넷 (http://www.image-net.org/) 이라는데서 추최하는 ILSVRC (Large Scale Visual Recognition Competition) 이라는 대회에서, 주최측이 제시하는 그림을 누가 잘 인식하는지를 겨루는 대회이다.



<그림. 이미지넷 대회에 사용되는 이미지들 일부>


이 대회에서는 천만장의 이미지를 학습하여, 15만장의 이미지를 인식하는 정답률을 겨루게 된다. 매년 알고리즘이 향상되는데, 딥러닝이 주목 받은 계기가된 AlexNet은 12년도 우승으로, 8개의 계층으로 16.4%의 에러율을 내었고, 14년에는 19개 계층을 가진 VGG 알고리즘이 7.3%의 오차율을 기록하였고, 14년에는 구글넷이 22개의 레이어로 6.7%의 오차율을 기록하였다. 그리고 최근에는 마이크로소프트의 152개의 레이어로 ResNet이 3.57%의 오차율을 기록하였다. (참고로 인간의 평균 오류율은 5% 내외이다.)

현재는 ResNet을 가장 많이 참고해서 사용하고 있고, 쉽게 사용하려면 VGG 모델을 사용하고 있다.




결론

머신러닝과 딥러닝에 대해서 공부를 하면서 이게 더이상 수학자나 과학자만의 영역이 아니라 개발자도 들어갈 수 있는 영역이라는 것을 알 수 있었고, 많은 딥러닝과 머신러닝 강의가 복잡한 수학 공식으로 설명이 되지만, 이건 아무래도 설명하는 사람이 수학쪽에 배경을 두고 있기 때문 일것이고, 요즘은 텐서플로우 프레임웍을 사용하면 복잡한 수학적인 지식이 없이 기본적인 머신러닝에 대한 이해만을 가지고도 머신러닝 알고리즘을 개발 및 서비스에 적용이 가능한 시대가 되었다고 본다.


그림 출처 및 참고 문서



저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

딥러닝의 개념과 유례

빅데이타/머신러닝 | 2016.11.27 20:37 | Posted by 조대협


딥러닝의 역사와 기본 개념

조대협 (http://bcho.tistory.com)

인경 신경망 알고리즘의 기본 개념


알파고나 머신러닝에서 많이 언급되는 알고리즘은 단연 딥러닝이다.

이 딥러닝은 머신러닝의 하나의 종류로 인공 신경망 알고리즘의 새로운 이름이다.


인공 신경망은 사람의 두뇌가 여러개의 뉴론으로 연결되서 복잡한 연산을 수행한다는데서 영감을 받아서, 머신러닝의 연산을 여러개의 간단한 노드를 뉴론 처럼 상호 연결해서 복잡한 연산을 하겠다는 아이디어이다.


<출처 : http://webspace.ship.edu/cgboer/theneuron.html >


이 뉴런의 구조를 조금 더 단순하게 표현해보면 다음과 같은 모양이 된다.


뉴런은 돌기를 통해서 여러 신경 자극 (예를 들어 피부에서 촉각)을 입력 받고, 이를 세포체가 인지하여 신호로 변환해준다. 즉 신경 자극을 입력 받아서 신호라는 결과로 변환해주는 과정을 거치는데,


이를 컴퓨터로 형상화 해보면 다음과 같은 형태가 된다.


뉴런의 돌기처럼 외부에서 입력값 X1,X2,X3를 읽어드리고, 이 입력값들은 돌기를 거치면서 인식되어 각각 W1*X1, W2*X2, W3*X3로 변환이 되어 세포체에 도착하여 여러 돌기에서 들어온 값은 (W1*X1+W2*X2+W3*X3)+b 값으로 취합된다.

이렇게 취합된 값은 세포체내에서 인지를 위해서 어떤 함수 f(x)를 거치게 되고, 이 값이 일정 값을 넘게 되면, Y에 1이라는 신호를 주고, 일정값을 넘지 않으면 0이라는 값을 준다.


즉 뉴런을 본떠서 입력값 X1...n에 대해서, 출력값  Y가 0 또는 1이 되는 알고리즘을 만든것이다.

Perceptron


이를 수식을 사용하여 한번 더 단순화를 시켜보면

X를 행렬이라고 하고,  X = [X1,X2,X3] 라고 하자.

그리고 역시 이에 대응되는 행렬 W를 정의하고 W=[W1,W2,W3] 라고 하면


<뉴런을 본떠서 만든 Perceptron>


입력 X를 받아서 W를 곱한 후에, 함수 f(x)를 거쳐서 0 또는 1의 결과를 내는 Y를 낸다.

즉 입력 X를 받아서 참(1)인지 거짓(0) 인지를 판별해주는 계산 유닛을 Perceptron이라고 한다.


이 Perceptron은 결국 W*X+b인 선을 그려서 이 선을 기준으로 1 또는 0을 판단하는 알고리즘이다.

예를 들어서 동물의 크기 (X1)와 동물의 복종도 (X2)라는 값을 가지고, 개인지 고양이인지를 구별하는 Perceptron이 있을때,

W*X+b로 그래프를 그려보면 (X=[X1,X2], W=[W1,W2] 다음과 같은 직선이 되고, 이 직선 윗부분이면 개, 아랫 부분이면 고양이 식으로 분류가 가능하다.



이 Perceptron은 입력에 따라서 Y를 1,0으로 분류해주는 알고리즘으로 앞에서 설명한 로지스틱 회귀 알고리즘을 사용할 수 있는데, 이때 로지스틱 회귀에서 사용한 함수 f(x)는 sigmoid 함수를 사용하였기 때문에, 여기서는 f(x)를 이 sigmoid 함수를 사용했다. 이 함수 f(x)를 Activation function이라고 한다. 이 Activation function은 중요하니 반드시 기억해놓기 바란다.


( 참고. 손쉬운 이해를 위해서 로지스틱 회귀와 유사하게 sigmoid 함수를 사용했지만,  sigmoid 함수이외에 다양한 함수를 Activation 함수로 사용할 수 있으며, 요즘은 sigmoid 함수의 정확도가 다른 Activation function에 비해 떨어지기 때문에, ReLu와 같은 다른 Activation function을 사용한다. 이 Activation function)에 대해서는 나중에 설명하겠다.)


Perceptron의 XOR 문제

그런데 이 Perceptron는 결정적인 문제를 가지고 있는데, 직선을 그려서 AND,OR 문제를 해결할 수 는 있지만,  XOR 문제를 풀어낼 수 가 없다는 것이다.


다음과 같은 Perceptron이 있을때


다음 그림 처럼 AND나 OR 문제는 직선을 그려서 해결이 가능하다.


그러나 다음과 같은 XOR 문제는 WX+b의 그래프로 해결이 가능할까?



<그림 XOR 문제>



하나의 선을 긋는 Perceptron으로는 이 문제의 해결이 불가능하다.


MLP (Multi Layer Perceptron) 다중 계층 퍼셉트론의 등장

이렇게 단일 Perceptron으로 XOR 문제를 풀 수 없음을 증명되었는데, 1969년에 Marvin Minsky 교수가, 이 문제를 해결 하는 방법으로 Perceptron을 다중으로 겹치면 이 문제를 해결할 수 있음을 증명하였다.



<그림 Multi Layer Perceptron의 개념도>


그런데, 이 MLP 역시 다른 문제를 가지고 있는데, MLP에서 학습을 통해서 구하고자 하는 것은 최적의 W와 b의 값을 찾아내는 것인데, 레이어가 복잡해질 수 록, 연산이 복잡해져서 현실적으로 이 W와 b의 값을 구하는 것이 불가능 하다는 것을 Marvin Minsky 교수가 증명 하였다.

Back Propagation 을 이용한 MLP 문제 해결

이런 문제를 해결 하기 위해서 Back propagation이라는 알고리즘이 도입되었는데, 기본 개념은

뉴럴 네트워크를 순방향으로 한번 연산을 한 다음에, 그 결과 값을 가지고, 뉴럴 네트워크를 역방향 (backward)로 계산하면서 값을 구한다는 개념이다.


Backpropagation의 개념에 대해서는 다음글에서 자세하게 설명하도록 한다.


Back Propagation 문제와 ReLu를 이용한 해결

그러나 이 Back Propagation 역시 문제를 가지고 있었는데, 뉴럴 네트워크가 깊어질 수 록 Backpropagation이 제대로 안된다.

즉 순방향(foward)한 결과를 역방향(backward)로 반영하면서 계산을 해야 하는데, 레이어가 깊을 수 록 뒤에 있는 값이 앞으로 전달이 되지 않는 문제 이다. 이를 Vanishing Gradient 문제라고 하는데, 그림으로 개념을 표현해보면 다음과 같다.

뒤에서 계산한 값이 앞의 레이어로 전달이 잘 되지 않는 것을 표현하기 위해서 흐리게 네트워크를 표현하였다.



이는 ReLu라는 activation function (앞에서는 sigmoid 함수를 사용했다.)으로 해결이 되었다.


뉴럴 네트워크의 초기값 문제

이 문제를 캐나다 CIFAR 연구소의 Hinton 교수님이 “뉴럴네트워크는 학습을 할때 초기값을 잘 주면 학습이 가능하다" 라는 것을 증명하면서 깊은 레이어를 가진 뉴럴 네트워크의 사용이 가능하게 된다.

이때 소개된 알고리즘이 초기값을 계산할 수 있는 RBM (Restricted Boltzmann Machine)이라는 알고리즘으로 이 알고리즘을 적용한 뉴럴 네트워크는 특히 머신러닝 알고리즘을 테스트 하는 ImageNet에서 CNN (Convolutional Neural Network)가 독보적인 성능을 내면서 뉴럴 네트워크가 주목 받기 시작하였다.


딥러닝

딥러닝이라는 어원은 새로운 알고리즘이나 개념을 이야기 하는 것이 아니고, 뉴럴 네트워크가 새롭게 주목을 받기 시작하면서 Hinton 교수님 등이 뉴럴네트워크에 대한 리브랜딩의 의미로 뉴럴 네트워크를 새로운 이름 “딥러닝"으로 부르기 시작하면서 시작 되었다.


추가

뉴럴네트워크와 딥러닝의 대략적인 개념과 역사에 대해서 알아보았다.

이 글에서는 뉴럴 네트워크에 대한 대략적인 개념만을 설명하고 있는데, 주로 언급되는 단어를 중심으로 기억하기를 바란다.

  • Perceptron

  • MLP (Multi Layer Perceptron)

  • Back propagation

  • ReLu

  • RBM


이외에도, Drop Out, Mini Batch, Ensemble 과 같은 개념이 있는데, 이 개념은 추후에 다시 설명하고, 딥러닝에서 이미지 인식에 많이 사용되는 CNN (Convolutional Neural Network)을 나중에 소개하도록 하겠다.



저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
 

티스토리 툴바