이전 편에서는 Scrapy 를 사용하여 크롤러를 제작하는 방법과 텔레그램 봇을 이용하여 배포하는 과정을 설명했고, 이번 편에서는 뉴스 봇에서 작동하고 있는 Natural Language Processing(자연어처리)을 어떻게 구현하였는지를 설명하고 있습니다.

시간이 없어서 한글로는 작성하지 못했습니다.

영어로 작성된 글은 아래링크에서 보실 수 있습니다.

 

Scrape News and Corporate Announcements in Real-Time — 3(NLP)

Previous posts can be found in the links below.

charliethewanderer.medium.com

 

'개인프로젝트' 카테고리의 다른 글

인공지능 딥러닝으로 사람 얼굴 평가하기  (0) 2021.01.29
블로그 이미지

찰리와마약공장

,

http://charliethewanderer1.ddns.net/faceeva/

 

http://charliethewanderer1.ddns.net/faceeva/

 

charliethewanderer1.ddns.net

완성된 형태의 어플리케이션을 웹을 통해 서비스 하고 있다. 위의 링크를 따라 들어가면 테스트 해볼 수 있다.
20~30대의 한국인 얼굴 500장의 이미지로 학습된 모델이라 경우에 따라 정확하지 않을 수 있으니, 결과값에 실망하지 말자.

 

English Version: Score Faces with Deep Learning. http://charliethewanderer1.ddns.net/face… | by Charlie_the_wanderer | Feb, 2021 | Medium

 

Score Faces with Deep Learning

http://charliethewanderer1.ddns.net/faceeva/

charliethewanderer.medium.com

 

 이 프로젝트는 작년 9월 경에 시작하여 약 3주 가량 소요된 프로젝트다. Coursera를 통해 파이썬, 머신러닝, 딥러닝에 대한 개념을 배우면서, 직접 개발과 배포를 해보고자, 이미지 인식과 관련된 주제를 정하여 프로젝트를 시작했다.

 

 처음에는 '정수리와 이마 이미지를 통해 탈모 진단하기' 를 비롯하여 기타 몇몇 아이디어들이 있었다. 여러 아이디어를 바탕으로 각각에 맞는 데이터셋을 구하기 위해 이런 저런 시도를 하다보니, 데이터셋을 구축하는 것이 생각보다 굉장히 까다로운 과정이라는 것을 깨달았다. 예를 들어, 탈모를 진단하는 모델을 만드는 경우에는, 탈모를 진단 할 수 있는 부위가 찍힌 이미지가 필요하고, 해당 이미지 속 사람이 탈모인지 아닌지를 진단하여 레이블 값을 제공할 수 있어야만 했다. 그러나, 그러한 이미지를 구하기도 어려웠을 뿐더러, 내가 의사가 아니기에, 탈모를 직접 진단할 수가 없었다. 마찬가지로, 대부분의 경우, 특정 주제와 관련된 데이터셋을 구축하는게 매우 어려웠기 때문에, 비교적 데이터셋을 구축하기 쉬운 '딥러닝으로 사람얼굴 평가하기' 라는 주제를 선택하게 되었다.

 

모델 개발 과정

 데이터를 수집하는 방법은 소위 노가다였다. 구글의 검색결과에 보이는 모든 사람의 이미지를 모조리 다운로드하고, 가장 못생긴 사람을 1, 가장 잘생긴(이쁜) 사람을 5의 레이블로 점수를 메기어 총 5단계의 class로 나누었다. 다만, 추후 모델 predict시, 휴대폰 카메라로 찍힌 이미지가 input이 될 것을 감안했기 때문에, 모델 학습에 쓰일 이미지 또한 최대한 고화질로 수집했으며, 여자, 남자, 레이블 별로 균일하게 50장씩 수집하여, balanced dataset이 되도록 데이터셋을 구축했다. 그러나 이 과정에서도, 여러가지 고려할점이 많았다.

 우선, 사람은 사실, 사람의 얼굴을 평가할때 얼굴만을 가지고 평가하는 것이 아니라는 것이었다. 사람은, 얼굴을 평가할때, 그 사람의 옷, 헤어스타일, 체격, 그 사람의 평판 등 얼굴 이외의 외부정보들을 무의식적으로 의사결정에 포함하게 된다.

 예를 들어, 연예인 유재석과, 유재석과 똑같이 생긴 일반인 유재석이 있다고 가정해 보자. 당신은 어느 사람에게 좀더 호감이 가겠는가? 둘의 얼굴은 똑같을지라도, 두 사람에게 느끼는 감정과 느낌은 당연히 다를 수밖에 없을 것이다.

따라서, 이러한 외부요인들로 인한 영향을 최소화 하기 위해서는, 이미지의 레이블을 평가할 때 이를 의식하고 최대한 객관적인 자세에서 평가해야 했다. 물론, 나의 주관이 당연히 반영될 수밖에 없었지만 말이다.

 또한, 모델의 학습에 쓸 이미지도 별도의 전처리를 해야 했다. 모든 이미지가 얼굴만 나와있는 것은 당연히 아니었기 때문에, 얼굴을 제외한 옷, 배경 등을 제외하고자 Python 의 opencv 라이브러리를 활용해서 수집된 이미지에서 얼굴만을 인식시켜 크롭하는 과정을 아래와 같이 추가했다.

def get_face_location(image_path):
    
    #Load the cascade
    face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')

    #Read the input image
    image_path = image_path
    img = cv2.imread(image_path)

    #Convert into grayscale
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    #Detect faces
    faces = face_cascade.detectMultiScale(gray_img, 1.03, 6)
    
    #Check whether there are more than one face or not
    detecting_result = 2 # 2 is normal
    if faces == ():
        detecting_result = 0 # "There's no face detected"
    elif len(faces) > 1:
        detecting_result = 1 # "There's more than one face detected"

    #Suppress other boxes
    max_box_coordinates = (0, 0, 0, 0)
    for (x, y, w, h) in faces:
        if w * h >= max_box_coordinates[2]*max_box_coordinates[3]:
            max_box_coordinates = (x, y, w, h)

    return (max_box_coordinates, img, detecting_result)

 나의 경우, 얼굴인식을 위해 opencv 의 haarcascade 알고리즘을 사용했다. 얼굴인식 알고리즘은 haarcascade 말고도 Convolutional Neural Network를 활용하는 MTCNN과 SSD 등의 다양한 알고리즘들이 있다. 

haarcascade알고리즘에 대한 자세한 설명은 OpenCV: Cascade Classifier 에서 확인이 가능하다.

 

OpenCV: Cascade Classifier

Next Tutorial: Cascade Classifier Training Goal In this tutorial, We will learn how the Haar cascade object detection works. We will see the basics of face detection and eye detection using the Haar Feature-based Cascade Classifiers We will use the cv::Cas

docs.opencv.org

  각 알고리즘은 장단점이 존재한다. haarcascade 알고리즘은 CNN과 같은 최신 딥러닝 기술을 활용하지 않기 때문에, MTCNN과 같은 최신 알고리즘보다 성능이 낮고, 얼굴의 각도가 틀어지거나, 얼굴의 일부가 가려지는 경우와 같은 돌발상황에서는 잘 인식하지 못하는 편이다. 그러나, 최신 알고리즘에 비해 필요로 하는 연산량이 훨씬 적기 때문에, 

 같은 시간에 훨씬 많은 이미지를 처리해 낼 수 있고, 라즈베리파이와 같은 초소형 컴퓨팅 칩에서도 쾌적하게 돌아간다는 장점을 갖고 있다. 이것이 내가 haarcascade를 선택한 중요한 이유 중 하나이다.

 특히, 나의 프로젝트에서는, 사용자가 직접 자신의 얼굴을 촬영하여 모델에 전송해 주는 것을 가정하고 있으므로, 사용자 또한 얼굴을 올바르게 정면에서 찍어야 한다는 것을 인지하고 있다는 가정이 가능하다. 또한, 라즈베리파이를 서버로 사용했으므로, 컴퓨팅 성능이 제한적이었다. 따라서, 돌발상황에 대한 Generalize성능보다는, 많은 사용자의 사진을 최대한 신속하고 빠르게 처리할 수 있는 효율성이 더 중요했다.

 

이미지 크롭과정을 설계한 이후에, 본격적인 모델 설계작업을 시작했다. 

 구축된 데이터셋과 레이블을 바탕으로 모델 학습을 시키려고 생각해보니, 성별과 관련된 문제가 남아있었다. '남자와 여자를 구분하지 않고, 학습시켜야 하나?', '남자와 여자를 구분하여 학습을 시킬까?', 어느 쪽도 딱히 틀리거나 정답은 아니었다. 곰곰히 생각해 본 결과, 사람의 사고방식을 흉내내기로 했다. 사람은 상대의 얼굴을 평가할 때, 상대방이 여자인지 남자인지 먼저 판단한 후, 얼굴을 평가하게 된다. 즉, 같은 얼굴이더라도 상대방이 남자라고 생각하고 보는 것과, 여자라고 생각하고 보는 것의 느낌과 평가결과는 크게 다르다는 것이다. 

 또한, 인공지능이 남자와 여자를 구분하는 것을 보여주는 것이 사용자로 하여금 좀더 재미와 호기심을 유발할 것이라고 생각하여, 성별을 구분하는 모델을 얼굴평가 모델에 앞에 이어 붙였다. 즉, 본격적인 얼굴평가는 성별이 먼저 구분된 이후에 이루어지도록 말이다.

Pipeline

딥러닝 모델은 tensorflow를 이용하여 구현했다. 처음에는 아래와 같이 직접 코드를 작성하여, 구축한 데이터셋으로 학습시켰지만, 유의미한 training set정확도와 dev set정확도가 나오지 않았다. 그도 그럴 것이 데이터셋이 겨우 사진 500장으로 구성되어, 학습량이 매우 부족했기 때문이었다.

def get_model():
     inputs = tf.keras.layers.Input(shape=(350, 350, 3))
    h = tf.keras.layers.Conv2D(64, (3, 3), activation='relu')(inputs)
    h = tf.keras.layers.MaxPooling2D((3, 3))(h)
    h = tf.keras.layers.BatchNormalization()(h)
    h = tf.keras.layers.Conv2D(128, (3, 3), activation='relu')(h)
    h = tf.keras.layers.MaxPooling2D((3, 3))(h)
    h = tf.keras.layers.Conv2D(128, (3,3), activation='relu')(h)
    h = tf.keras.layers.MaxPooling2D((3,3))(h)
    h = tf.keras.layers.Dropout(0.15)(h)
    h = tf.keras.layers.Flatten()(h)
    h = tf.keras.layers.Dense(128, activation='relu')(h)
    h = tf.keras.layers.Dense(256, activation='relu')(h)
    outputs = tf.keras.layers.Dense(5, activation='softmax')(h)
    
    model = Model(inputs=inputs, outputs=outputs)
    model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])    
    model.summary()
    
    return model

 부족한 데이터셋을 극복하기 위한 방법은 크게 세가지가 있다.

 첫번째는, 그냥 데이터를 더 모아오는 것이다. 굉장히 무식해 보이지만, 사실 이 방법이 유일한 방법이거나, 생각보다 가장 빠른방법인 경우가 많다.

 두번째는, 데이터셋을 합성(synthesize)해 내는 것이다. tensorflow에서는 이것이 쉽게 가능한데, ImageDataGenerator 와 같은 클래스를 이용하면, 데이터셋에 이미있는 이미지에 명암, 기울기, 크롭, 좌우반전 등의 다양한 변화를 가한 새로운 이미지를 생성시켜 준다. 다만, 이는 절대로 만능이 아니다. 모델이 좀더 예상치 못한 데이터에 generalize 할 수 있도록 도와주는 도구 수준이라고 이해하는 것이 옳다. 

 마지막은, Transfer Learning(전이학습)이 가능하면, 이를 활용하는 것이다. Transfer Learning은 인간으로 비유하면, 원래 알고 있던 지식을 응용하여 다른 곳에 써먹는 것으로 생각하면 이해가 쉽다.

 이미지 인식을 활용하는 딥러닝 모델들은 궁극적인 목표작업은 다를지라도, 밑단의 레이어에서 일어나는 작업들은 비슷한 경우가 많다. 예를 들어, 강아지와 고양이를 구분하는 이미지 인식 모델이 있다고 가정해 보자. 궁극적인 목표작업은 강아지와 고양이를 구분해 내는 것이지만, 밑단의 레이어에서는 사물의 경계선을 구분해 내거나 명암을 구분해 내는 작업들이 일어난다. 즉, 인간의 얼굴을 구분하는 것이든, 고양이와 강아지를 구분하는 것이든, 이미지를 분석하는 과정에서 일어나는 기초작업들은 비슷하다는 것이다. 따라서, 이미 학습된 다른 이미지 인식 모델의 밑부분만 가져와서 윗부분에 내가 하고자 하는 목표 작업 레이어만 붙여버린후, 내가 갖고 있는 데이터로 또 한번 학습시키면, 이것이 바로 Transfer Learning이 되는 것이다. 당연히 비슷한 종류의 목표작업이나, 이미지를 학습했던 모델을 가져오는 것이 가장 Transfer Learning을 하기에 좋으며, 어느 부분까지 사용하고 어느 부분부터는 잘라서 버릴 것인지 선택하는 것은 사용자의 선택에 달려있다.

 

 나는 MobilenetV2 라는 딥러닝 모델을 사용하기로 했다. 모델에 대한 자세한 설명과 사용방법은 다음 링크에서 확인할 수 있다. Tf2 Preview | Mobilenet V2 | Feature Vector | TensorFlow Hub (tfhub.dev)

 

TensorFlow Hub

 

tfhub.dev

MobilenetV2에 대한 간략한 설명은 다음과 같다.

MobileNet V2 is a family of neural network architectures for efficient on-device image classification and related tasks.

소형 디바이스에서의 이미지 분류와 관련된 작업을 위한 Neural network 아키텍쳐의 종류중 하나라고 소개를 하고 있다. 즉, MobileNetV2는, 개발부터 스마트폰, 라즈베리파이 등의 소형 컴퓨터에서의 작동을 염두에 두고 개발된 효율성 위주의 아키텍처라고 볼 수 있다. 또한 ILSVRC-2012-CLS 데이터셋으로 image classification 작업이 이미 학습되어있는 상태로, 사용자는 이 모델을 그대로 불러와서, 원하는 부분만 잘라내어 사용할 수 있다.

#fetch mobilenet_v2 model
def get_mobilenet_model():
    mobilenet_v2 = hub.KerasLayer("https://tfhub.dev/google/tf2-preview/mobilenet_v2/feature_vector/4", output_shape=[1280], trainable=False)
    
    model = tf.keras.Sequential([
        tf.keras.layers.InputLayer(input_shape=(224, 224, 3)),
        mobilenet_v2,
        tf.keras.layers.Dropout(0.2),
        tf.keras.layers.Dense(5, activation='softmax')
    ])
    
    model.build((None,)+(224,224,3))
    model.summary()
    
    return model

Transfer Learning을 이용하면 윗부분만 갈아치우면 되기 때문에, 코드가 굉장히 간략해진다. 

두번째 줄에 있는 KerasLayer method에 'trainable=False'를 지정함으로써, mobilenet의 output 레이어를 날려버린 채로 불러왔다. 그리고, 얼굴을 총 5개의 단계로 분류하는 것이 최종 목표이므로, 5개의 unit을 갖고 있는 Dense 레이어를 mobilenet 모델위에 올렸다.

 

마찬가지로, 성별 구분을 위한 딥러닝 모델도 Mobilenet을 활용하여 다음과 같이 만들었다.

유일한 차이점은, 성별 구분은 binary classification(이진분류) 이기 때문에 output layer의 유닛 수가 1이고, activation 함수가 softmax가 아닌 sigmoid 함수라는 것이다.

#fetch mobilenet_v2 model for binary classification
def get_mobilenet_model_gender():
    mobilenet_v2 = hub.KerasLayer("https://tfhub.dev/google/tf2-preview/mobilenet_v2/feature_vector/4", output_shape=[1280], trainable=False)
    
    model = tf.keras.Sequential([
        tf.keras.layers.InputLayer(input_shape=(224, 224, 3)),
        mobilenet_v2,
        tf.keras.layers.Dropout(0.2),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    
    model.build((None,)+(224,224,3))
    model.summary()
    
    return model
    

 

이제 모델 트레이닝을 위한 대부분의 작업이 끝났다. 

 

directory_man_train = 'dataset/cropped_train_set/man'
directory_man_val = 'dataset/cropped_val_set/man'

datagen = ImageDataGenerator(rotation_range=20,
                            horizontal_flip=True,
                            brightness_range=[0.6, 1.3],
                            shear_range=0.15,
                            rescale=(1./255.))

train_generator_man = datagen.flow_from_directory(directory_man_train,
                                             target_size=(224, 224),
                                             batch_size=1,
                                             class_mode='sparse')

val_datagen = ImageDataGenerator(rescale=(1./255.))

val_generator_man = val_datagen.flow_from_directory(directory_man_val,
                                                   target_size=(224, 224),
                                                   batch_size=1,
                                                   class_mode='sparse')

 위의 코드는 이미지 500장에 대한 합성데이터를 만들어 내기 위한 과정이다.

내가 보유하고 있는 데이터의 숫자가 절대적으로 부족했기 때문에,

모델 overfitting의 가능성을 낮추기 위한 데이터합성 과정은 필수적이었다.

tensorflow의 ImageDataGenerator 클래스는 기존 이미지에 명함변화, 휘도변화, 색도변화, 기울기변화, 크롭 등을 적용시킨 새로운 이미지를 메모리 상에서 생성하여, 모델의 학습 데이터로 활용할 수 있게 해 준다. 아래의 이미지를 보면, 무슨 말인지 쉽게 이해가 갈 것이다.

이미지 1장이 12장으로 뻥튀기 되었지만, 사실은 모두 같은 이미지다.

 

데이터셋의 양이 많지 않았기 때문에 CPU를 이용하여 학습을 시키더라도 성별분류, 남자얼굴분류, 여자얼굴분류 총 3개의 모델을 학습시키는데에 10분도 걸리지 않았다. 그러나, 결과는 놀라웠다. 겨우 500장의 이미지로 학습을 했을 뿐인데, validation 정확도가 80%를 넘었다. 특히 놀라웠던 부분은, 성별 분류의 경우 정확도가 90% 이상이었다는 것이다. 

 또한, 애초에 balanced dataset 으로 학습시켰기 때문에, accuracy 이외에 precision 혹은 recall, roc auc score 등의 metric들도 이상이 없었다.

 사람의 눈으로 보아도 남성 같이 생긴 여성의 경우, 남성일 확률이 크게 올라갔고, 그 반대도 마찬가지였다. validation set 안에 있는 이미지가 아닌, 아예 데이터셋에 없는 이미지를 여러번 모델에 집어넣어본 결과, 내가 보는 눈과 크게 다르지 않아서, 모델의 generalize 성능도 500장으로 학습된 것 치고는 꽤 괜찮다는 것을 느낄 수 있었다.

  

 

모델 배포 과정

 이제, '만들어진 모델을 라즈베리파이에 올리기만 하면 되겠구나!' 라는 기쁨에 찬 생각을 했다.

그러나 이는 곧 절망으로 바뀌게 된다. 사실, 이제부터가 시작이었던 것이다. 

 

 라즈베리파이는 데비안 리눅스 기반의 라즈비안OS로 돌아가기 때문에, 리눅스를 어느정도 사용할 줄 알아야만 했다. 뿐만아니라, 라즈베리파이는 모니터가 없기 때문에 VSCODE의 SSH 원격 접속을 통해 CLI 환경에서 모든 것을 해결해야 했다. 또한, X86 아키텍처가 아닌 ARM 아키텍처의 CPU를 사용하기 때문에, 패키지 호환성에 대한 문제도 존재했다.

이제까지 윈도우의 GUI환경에만 익숙했던 나는, 리눅스, VSCODE, CLI 환경, ARM환경, 이 모든 것을 동시에 배우느라 정말 진땀을 뺐다. 거기에 conda 가상환경까지 사용하게 되니, 한 발자국을 내딛을 때마다 수많은 에러메세지와 마주치면서, 포기하고 싶을 때가 한 두번이 아니었다.

 

'도대체 왜 굳이 리눅스를 쓰는거야?', '가상환경은 대체 왜 만들어?', '대체 내가 설치한 파일은 어디에 있는거야?' , '내 컴퓨터인데 왜 자꾸 권한이 없데?', 'ARM은 안 된다네.. 그냥 노트북으로 할까?' 등의 수많은 질문과 답답함이 머릿속을 가득 채웠다.

 방법을 찾다 찾다 지치면, 한숨 자고 일어나서 다시 도전해 보고, '아직 못 찾은거지, 방법은 있을거야' 라는 마인드로 전세계 웹사이트를 뒤지면서 문제를 해결해 나갔다. 그리고 결과적으로는 내 생각이 맞았다. 누군가 나보다 먼저 물어본 사람이 항상 존재했다. 

 이러한 과정을 거치면서 지금은 내가 갖고있던 모든 질문들이 스스로 대답이 되었고, 이때 힘들게 배운 경험들과 지식들을 정말 잘 써먹고 있다. 그러나 다시는 돌아가고 싶지 않은 경험이다. (도커를 최대한 활용하자..)

 우선 라즈베리파이에 내가 만든 모델을 배포하기 이전에, 라즈베리파이에 웹서버와 웹 어플리케이션을 구동할 수 있는 프레임워크가 있어야만 했다. 웹 프레임 워크는 기존에 Coursera에서 배운 Python 기반의 Django 프레임워크를 활용하기로 하였다. 다행히, Django를 설치하는데에 필요한 모든 패키지와 라이브러리들이 ARM 리눅스와 잘 호환이 됐다.

 페이지의 디자인과 데이터의 입출력을 위한 CSS, HTML, 그리고 Django 데이터베이스 작업이 어느정도 마무리가 된 이후, tensorflow를 설치하려고 했는데, 이게 웬걸? 아직 tensorflow는 라즈베리파이를 지원하지 않는단다. 웬 청천벽력같은 소리인가? 

 잘 찾아보니, tensorflow lite를 이용하면 tensorflow 모델의 학습은 아니더라도 실행(inference)은 가능하다는 것을 깨닫고, 내가 만든 모델을 tesnorflow lite 코드로 다음과 같이 변환해주었다. 

#Load gender classifying model
model_gender = tf.keras.models.load_model('./savedmodel/gender_checkpoint')
#Load scoring model for a man 
model_man = tf.keras.models.load_model('./savedmodel/man_checkpoint')
#Load scoring model for a woman
model_woman = tf.keras.models.load_model('./savedmodel/woman_checkpoint')

converter = tf.lite.TFLiteConverter.from_saved_model('./savedmodel/gender_checkpoint')
tflite_model = converter.convert()
open("converted_model_gender.tflite", "wb").write(tflite_model)

converter = tf.lite.TFLiteConverter.from_saved_model('./savedmodel/man_checkpoint')
tflite_model = converter.convert()
open("converted_model_man.tflite", "wb").write(tflite_model)

converter = tf.lite.TFLiteConverter.from_saved_model('./savedmodel/woman_checkpoint')
tflite_model = converter.convert()
open("converted_model_woman.tflite", "wb").write(tflite_model)

tensorflow 모델을 tensorflow lite 모델로 변환하는 것은 매우 간단했다. 저장된 tensorflow 모델을 불러들인 이후, tensorflow lite로 변환시켜주는 method를 사용하면 끝이다.

 

그 이후, 라즈베리파이 서버로, 저장된 tflite 모델 파일과 코드를 옮기고, Django의 View에 연결시키면 배포준비가 완료된다. 자바스크립트를 다룰 줄 알면, 데이터의 입출력과정의 일부를 프론트엔드에서 해결할 수도 있지만, 나의 경우에는 아직 자바스크립트에 익숙하지 않아서, 대부분의 연산과 어플리케이션 작동이 Django의 views.py 에서 일어나도록 설계하였다. 즉, Python 코드에서 모든 백엔드 연산이 처리된 이후, 결과값만을 전달하도록 만들었다. 다만 인식된 얼굴에 사각형을 그리는 작업의 경우는, 백엔드에서 받아온 좌표값을 이용하여 프론트엔드에서 처리하도록 만들었다.

import numpy as np
import tflite_runtime.interpreter as tflite
import cv2
from PIL import Image
import os

base_dir = os.path.dirname(os.path.abspath(__file__))

#Load TFLite models
interpreter_gender = tflite.Interpreter(os.path.join(base_dir, 'converted_model_gender.tflite'))
interpreter_man = tflite.Interpreter(os.path.join(base_dir, 'converted_model_man.tflite'))
interpreter_woman = tflite.Interpreter(os.path.join(base_dir, 'converted_model_woman.tflite'))

#Feed an interpreter and array data and get output_pred
def get_output_tflite(interpreter, input_data):
    
    input_details = interpreter.get_input_details()
    output_details = interpreter.get_output_details()
    interpreter.allocate_tensors()
    
    interpreter.set_tensor(input_details[0]['index'], input_data)
    interpreter.invoke()
    output_data = interpreter.get_tensor(output_details[0]['index'])
    return output_data

def get_face_location(image_path):
    
    #Load the cascade
    face_cascade = cv2.CascadeClassifier(os.path.join(base_dir,'haarcascade_frontalface_default.xml'))

    #Read the input image with PIL Image
    image_path = image_path
    img = Image.open(image_path)
    img = np.array(img)
    #rgb to bgr
    img = img[...,::-1]

    #Convert into grayscale
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    #Detect faces
    faces = face_cascade.detectMultiScale(gray_img, 1.03, 6)
    
    #Check whether there are more than one face or not
    detecting_result = 2 # 2 is normal
    if faces == ():
        detecting_result = 0 # "There's no face detected"
    elif len(faces) > 1:
        detecting_result = 1 # "There's more than one face detected"

    #Suppress other boxes
    max_box_coordinates = (0, 0, 0, 0)
    for (x, y, w, h) in faces:
        if w * h >= max_box_coordinates[2]*max_box_coordinates[3]:
            max_box_coordinates = (x, y, w, h)

    return (max_box_coordinates, img, detecting_result)

def cropped_image(image_ndarray, x, y, w, h, detecting_result):
    
    if detecting_result == 0:
        #print("There is no face detected")
        return image_ndarray
        
        
    else:
        cropped_img = image_ndarray[y:y+h, x:x+w]
        return cropped_img

def get_face_rank(pred):
    
    if pred == 0:
        string = "ㅆㅎㅌㅊ 입니다..힘내세요.."
    elif pred == 1:
        string = "ㅎㅌㅊ 입니다.."
    elif pred == 2:
        string = "ㅍㅌㅊ 입니다.."
    elif pred == 3:
        string = "ㅅㅌㅊ 입니다.."
    else:
        string = "ㅆㅅㅌㅊ 입니다. 축하합니다!"
    
    return string

def tflite_model_predict(image_path):
    image_path = image_path   

    face_coordinates, img, detecting_result = get_face_location(image_path)
    x, y, w, h = face_coordinates
    cropped_img_original = cropped_image(img, x, y, w, h, detecting_result)
    cropped_img = cv2.resize(cropped_img_original, (224, 224))
    cropped_img = cv2.cvtColor(cropped_img, cv2.COLOR_BGR2RGB)/255.
    
    pred_sex_num = get_output_tflite(interpreter_gender, np.array(np.expand_dims(cropped_img, axis=0), dtype=np.float32))
    pred_sex = np.round(pred_sex_num)
    # Classify gender first
    if pred_sex == 0:
        sex = 'man'
    else:
        sex = 'woman'
    # Scoring a face based on the gender
    if sex == 'man':
        pred = get_output_tflite(interpreter_man, np.array(np.expand_dims(cropped_img, axis=0), dtype=np.float32))
    else:
        pred = get_output_tflite(interpreter_woman, np.array(np.expand_dims(cropped_img, axis=0), dtype=np.float32))
    
    return pred, np.argmax(pred[0]), cropped_img_original, detecting_result, pred_sex_num, face_coordinates

 

완성된 어플리케이션의 작동화면은 다음과 같다.

원하는 사진을 고른후 UPLOAD를 클릭하면, 백엔드에서 딥러닝 모델이 작동하여 성별의 예측값과 얼굴평가의 예측된 레이블값 출력하여 준다. 소요시간은 1~2초 내외이며, 웬만한 스마트폰보다 성능이 낮은 라즈베리파이 위에서 작동한다는 것을 고려하면 굉장히 훌륭한 속도라고 생각된다.

레이블은 1점이 가장 안좋은 점수이며 5점이 가장 높은 점수이다.

500장의 20대 ~ 30대 한국인 이미지로 학습시켰기 때문에, 나이대가 다르거나, 인종이 다른 경우 정확한 예측이 어려울 수 있으며, 데이터셋의 숫자가 작아, 엉뚱한 결과가 나올수도 있기 때문에, 점수가 낮더라도 신경쓰지 않아도 된다.

 

 

프로젝트를 마무리하며 느낀 점

지금와서 돌아보면 별 것 아닌것처럼 느껴지지만, 당시에는 프로젝트를 진행하면서 어려움을 많이 겪었다.

 어려움은 크게 두 가지로 나눌 수 있었다.

 첫번째로는, 딥러닝을 제대로 활용하기 위해서는, 딥러닝 자체뿐만아니라 데이터와 데이터 전처리에 대한 개념들을 잘 숙지하고 있어야 한다는 것이었다. Coursera 에서의 수업과제를 할때는 이미 주제와 데이터셋이 준비가 되어있었기 때문에, 모델의 파라미터 설정을 통하여, 모델의 성능을 높이는 데에만 집중을 했었다. 그러나, 현실에서는, 문제 해결을 위한 데이터셋을 구축하고 모델의 파이프라인을 설계하는 것이 딥러닝 모델 자체의 성능을 높이는 것보다 더 중요했다.

 첫 단추를 잘 못 끼우면 그 뒤의 일들은 의미가 없어지기 때문이다. 특히, 데이터의 레이블을 메기는 과정 또한, 레이블을 메기는 사람의 주관과 판단 기준이 모델 학습에 직접적인 영향을 미칠 수 있으므로, 매우 신중하게 진행해야 했다는 점도, 데이터 자체의 중요성을 깨닫게 해주는 소중함 경험이 되었다.

 

 두번째로는, 배포를 하기 위해서는, 다른 분야의 지식도 충분히 알고 있어야 한다는 점이었다. 머신러닝과 딥러닝만 할줄 안다는 것은, 요리로 따지면, 요리의 레시피만 알고, 재료를 어디서 사는지, 어떻게 손질하는지는 모르는 것과 같다. 즉, 여러 분야를 두루두루 알고있어야 배포가 가능하다는 것이다. 이러한 이유로, 프로그래밍 언어 뿐만 아니라, 가상환경과 IDE를 활용하여 체계적인 개발환경을 구축하는 방법, 리눅스 운영체제와, DOM 모델과 같은 웹의 구조, Database, SQL의 기본구조, 네트워크 기초 등 많은 분야의 지식들을 빠르게 배워나가야 했고, 이러한 지식들 또한 머신러닝과 딥러닝 못지않게 중요하다는 것을 깨달을 수 있었다.

 

마지막으로 느낀 점은, 이미지를 활용하는 딥러닝 모델은 데이터셋을 구할 수 있는 환경이 매우 제한적이라는 것이다. 쉽게 scraping이 가능한 text 데이터와는 달리, 이미지 데이터는 scraping이 제한적이고, 데이터셋을 구축한다고 하더라도, 이미지의 특성상 용량이 크기 때문에, 개인이 많은 양의 데이터를 저장하여 사용하기가 어렵다는 것이다.

 이러한 데이터셋 구축의 어려움 때문에, 비교적 접근이 쉬운, text 데이터를 활용하는 Natural Language Processing에 더 많은 시간을 투자하는 것이 효율적일 것이라는 생각을 하는 계기가 되었다.

 

 

 

 

블로그 이미지

찰리와마약공장

,