17 minute read

Age / Gender Detection




Repository for this project




0. Introduction


  • 사람의 얼굴 Image로 성별 / 나이대를 판별하는 Model은 상점 등과 같은 고객의 통계 정보를 필요로 하는 곳에 유용하게 사용할 수 있을 것 같아서 이 Project를 진행하게 되었습니다.






0.1. Face Detector

  • K-Face Dataset에서 사람 얼굴 부분만을 추출하기 위해서 Face Detector를 사용하여야 합니다.

  • 여기서는 DNN Face Detector in OpenCV를 사용합니다.
  • Face Detector는 Input으로 Image를 넣어주면, 해당 Image에서 사람 얼굴이라고 판단되는 영역의 정보와 확신(신뢰)도를 값으로 Return해 줍니다.

  • Face Detector Pre-Trained Model을 사용하기 위해서는 아래 2개의 File을 받아야 합니다.
    • MODEL_FILE : opencv_face_detector_uint8.pb
    • CONFIG_FILE : opencv_face_detector.pbtxt
  • 속도도 빠르고 성능도 좋아서 Face Detection에 이 Module을 사용하도록 하겠습니다.

  • 자세한 사용 방법은 이후 Code로 살펴보겠습니다.



0.2. K-Face Dataset

  • K-Face Dataset는 AI Hub에서 제공하는 한국인 얼굴 Dataset입니다.

  • AI Hub는 한국지능정보사회진흥원이 운영하는 AI 통합 플랫폼이다.

  • 2017년 AI 학습용 데이터 구축·확산 사업의 일환으로 AI 기술·서비스 개발에 필수적인 AI데이터, 소프트웨어, 컴퓨팅자원, 소재정보 등을 원스톱으로 제공하는 것을 목적으로 합니다.

  • K-Face Dataset의 자세한 정보는 https://github.com/k-face/k-face_2019에서 확인할 수 있습니다.

  • 연령대별 / 성별 / 카메라 위치 / 액세사리 / 조명 / 표정 / 해상도 별로 다양하게 변경하면서 촬영한 얼굴 사진으로 구성되어 있습니다.






  • 이 Dataset을 얻기 위해서는 별도의 신청서를 작성하여 담당자에게 제출하여야 하며, 승인이 나면 추후에 Download 가능한 Link를 받을 수 있습니다.

  • 제가 사용한 K-Face Dataset에는 총 400명의 Image가 있었고, 이 중에 Train Data로써 사용할 수 없는 Image들은 사용하지 않도록 하겠습니다.

  • 각 Image에는 고유 ID가 부여되여 있고, 별도의 Meta Data File에는 해당 ID의 연령대 / 성별 정보가 제공됩니다.

  • 이를 활용하여 사진에 나오는 사람의 정보를 확인할 수 있고, 최종적으로 이를 이용하여 Train Data Set을 만들 것입니다.




1. Preprocessing



1.1. Prepare

  • 필요한 Package를 Load합니다.


import numpy as np
import pandas as pd
import os
import glob
from tqdm import tqdm
import cv2



  • 사진에서 얼굴 부분만을 추출하기 위해서 OpenCV DNN Face Detector를 사용하도록 하겠습니다.


MODEL_FILE = "opencv_face_detector_uint8.pb"
CONFIG_FILE = "opencv_face_detector.pbtxt"


  • OpenCV DNN Face Detector 관련 상수를 정의합니다.


‘CONFIDENCE_FACE = 0.9’ - Face Detector가 제공해주는 값으로 추출한 얼굴 부분이 어느 정도 신뢰도가 있는지 나타내주는 값입니다.


SIZE = 300
CONFIDENCE_FACE = 0.9


‘MARGIN_RATIO = 0.2’

  • OpenCV Face Detector는 얼굴 부분을 너무 Tight하게 Detect하는 경향이 있기 때문에, 상하좌우 약간의 Margin을 더 두어서 얼굴 부분으로 Detecr하도록 하겠습니다.
  • 여기서는 20% 정도 더 여유롭게 얼굴부분으로 Detect하도록 합니다.


MARGIN_RAIO = 0.2



1.2. Select Appropriate Face Images


  • Train Data로 사용하지 못하는 사진의 경우에 미리 배제하도록 하겠습니다.

  • 예를 들면, 선글라스 , 모자 등과 같이 얼굴을 많이 가리는 사진은 빼도록 하겠습니다.

  • 적절한 밝기의 사진만 사용하고, 카메라 위치가 정면 얼굴을 찍지 않은 경우도 배제하고, 찡그린 얼굴 표정도 사용하지 않도록 하겠습니다.




# 선글라스 , 모자 , 모자 + 뿔테안경을 착용한 경우는 모두 사용하지 않는다.
use_accessaries = ['S001' , 'S002' , 'S003']

# 적절한 밝기의 조명 사진만 사용한다
use_lights = ['L1' , 'L2' , 'L3' , 'L4' , 'L8' ,'L9' ,'L12' ,'L13' ,'L22']

# 얼굴이 제대로 보이지 않는 카메라 위치는 사용하지 않는다.
use_cam_pos = ['C4','C5','C6','C7','C8','C9','C10','C19','C20']

# 찡그린 표정의 사진은 사용하지 않는다.
use_looks = ['E01','E02']


dir_path = "../Dataset/High_Resolution"
data_file_path = []

def GetTrainDataFileList():
    for (root, directories, files) in tqdm(os.walk(dir_path)):
        for file in files:
            
            if '.jpg' in file:
                append = False
                
                file_path = os.path.join(root, file)
                for u in use_accessaries:
                    if u in str(file_path):
                        append = True
                        break

                if append:
                    append = False
                    for u in use_lights:
                        if u in str(file_path):
                            append = True
                            break

                if append:
                    append = False
                    for u in use_cam_pos:
                        if u in str(file_path):
                            append = True
                            break

                if append:
                    append = False
                    for u in use_looks:
                        if u in str(file_path):
                            append = True
                            break

                if append:
                    data_file_path.append( file_path )


    print( len(data_file_path) )

    meta_data = pd.DataFrame( data_file_path , columns=['file_path'])
    meta_data.to_csv("meta_data_K-Face.csv",index=False)


GetTrainDataFileList()
290801it [00:38, 7486.83it/s]


583200


  • 총 32400000장의 Image중에 조건에 부합하는 583200장의 Image를 Train Data로 사용하겠습니다.


meta_data = pd.read_csv("meta_data_K-Face.csv")



1.3. Extract Face Location Information


  • 선정한 Image File에서 얼굴부분에 대한 정보를 추출하도록 하겠습니다.

  • 이 부분에서 OpenCV DNN Face Detector를 사용하도록 하겠습니다.


file_path = meta_data['file_path'].tolist()
len(file_path)


583200


  • 신뢰도가 0.9 이상인 얼굴 부분만을 추출하여 상하좌우 여백을 추가하여 얼굴 위치 정보를 저장하도록 하겠습니다.


filename = []
left_list = []
right_list = []
top_list = []
bottom_list = []

net = cv2.dnn.readNetFromTensorflow( MODEL_FILE , CONFIG_FILE )

for file in tqdm(file_path):

    img = cv2.imread(file)
    rows, cols, channels = img.shape
    blob = cv2.dnn.blobFromImage(img, 1.0, (SIZE, SIZE))

    net.setInput(blob)
    detections = net.forward()

    for detection in detections[0, 0]:

        score = float(detection[2])

        if score > CONFIDENCE_FACE:

            if detection[3] >= 1.00 or detection[4] >= 1.00 or detection[5] >= 1.00 or detection[6] >= 1.00 or detection[3] <= 0 or detection[4] < 0 or detection[5] <= 0 or detection[6] <= 0:
                filename.append(np.NaN)
                left_list.append( np.NaN )
                right_list.append( np.NaN )
                top_list.append( np.NaN )
                bottom_list.append( np.NaN )

            else:
                left = int(detection[3] * cols)
                top = int(detection[4] * rows)
                right = int(detection[5] * cols)
                bottom = int(detection[6] * rows)

                left = left - int((right - left) * MARGIN_RAIO)
                top = top - int((bottom - top) * MARGIN_RAIO)
                right = right + int((right - left) * MARGIN_RAIO)
                bottom = bottom + int((bottom - top) * MARGIN_RAIO / 2)

                if left < 0:
                    left = 0

                if right > cols:
                    right = cols

                if top < 0:
                    top = 0

                if bottom > rows:
                    bottom = rows
                    
                filename.append(file)
                left_list.append( left )
                right_list.append( right )
                top_list.append( top )
                bottom_list.append( bottom )
100%|????????????????????????????????????????????????????????????????????????| 583200/583200 [4:05:05<00:00, 39.66it/s]


  • 추후에 Train에 사용하기 위해서 각 Image에서 얼굴부분의 위치를 저장합니다.


coor = pd.DataFrame( list(zip(filename , left_list , right_list , top_list , bottom_list)) , columns=['file_path' , 'left' , 'right' , 'top' , 'bottom'] )


coor.head()


file_path left right top bottom
0 ../Dataset/High_Resolution\19062421\S001\L1\E0... 289 491 155 402
1 ../Dataset/High_Resolution\19062421\S001\L1\E0... 377 581 175 392
2 ../Dataset/High_Resolution\19062421\S001\L1\E0... 313 502 181 401
3 ../Dataset/High_Resolution\19062421\S001\L1\E0... 394 602 142 407
4 ../Dataset/High_Resolution\19062421\S001\L1\E0... 373 589 142 403


coor.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 365672 entries, 0 to 365671
Data columns (total 5 columns):
 #   Column     Non-Null Count   Dtype 
---  ------     --------------   ----- 
 0   file_path  365672 non-null  object
 1   left       365672 non-null  int64 
 2   right      365672 non-null  int64 
 3   top        365672 non-null  int64 
 4   bottom     365672 non-null  int64 
dtypes: int64(4), object(1)
memory usage: 13.9+ MB
coor.to_csv("coor.csv" , index=False)



1.4. Additional Information


  • 마지막으로, 각 사진에 나오는 사람의 연령대 / 성별 정보도 추가합니다.

  • 이 정보는 Train시에 Label 정보로 이용합니다.

  • ID는 File Path에서 얻을 수 있으므로, File Path에서 ID만 추출해서 새 Column을 만듭니다.

def get_ID(file_path):
    ID = str(file_path)[27:35]
    return int(ID)


coor.head()


file_path left right top bottom
0 ../Dataset/High_Resolution\19062421\S001\L1\E0... 289 491 155 402
1 ../Dataset/High_Resolution\19062421\S001\L1\E0... 377 581 175 392
2 ../Dataset/High_Resolution\19062421\S001\L1\E0... 313 502 181 401
3 ../Dataset/High_Resolution\19062421\S001\L1\E0... 394 602 142 407
4 ../Dataset/High_Resolution\19062421\S001\L1\E0... 373 589 142 403


coor['ID'] = coor['file_path'].apply(get_ID)


  • 이 ID값을 Key로 하여 연령대 / 성별 정보를 추가합니다.
coor.head()


file_path left right top bottom ID
0 ../Dataset/High_Resolution\19062421\S001\L1\E0... 289 491 155 402 19062421
1 ../Dataset/High_Resolution\19062421\S001\L1\E0... 377 581 175 392 19062421
2 ../Dataset/High_Resolution\19062421\S001\L1\E0... 313 502 181 401 19062421
3 ../Dataset/High_Resolution\19062421\S001\L1\E0... 394 602 142 407 19062421
4 ../Dataset/High_Resolution\19062421\S001\L1\E0... 373 589 142 403 19062421


coor.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 365672 entries, 0 to 365671
Data columns (total 6 columns):
 #   Column     Non-Null Count   Dtype 
---  ------     --------------   ----- 
 0   file_path  365672 non-null  object
 1   left       365672 non-null  int64 
 2   right      365672 non-null  int64 
 3   top        365672 non-null  int64 
 4   bottom     365672 non-null  int64 
 5   ID         365672 non-null  int64 
dtypes: int64(5), object(1)
memory usage: 16.7+ MB



  • 연령 / 성별 정보는 별도의 File로 제공됩니다.


  • 이 File에는 ID값으로 연령 / 성별을 알 수 있습니다.


additional_info = pd.read_csv("KFace_data_information_Folder1_400.csv",encoding='CP949')
additional_info = additional_info[['ID','연령대','성별']]
print(additional_info.head())
print(additional_info.info())
         ID  연령대 성별
0  19062421  30대  남
1  19062431  40대  남
2  19062521  50대  여
3  19062531  40대  여
4  19062542  30대  여
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 400 entries, 0 to 399
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   ID      400 non-null    int64 
 1   연령대     400 non-null    object
 2   성별      400 non-null    object
dtypes: int64(1), object(2)
memory usage: 9.5+ KB
None


  • ID값을 Key로하여 연령대 / 성별 정보를 추가합니다.


merged_meta_data = pd.merge(coor,
         additional_info,
         how='left',
         on='ID'
        )
merged_meta_data.tail()


file_path left right top bottom ID 연령대 성별
365667 ../Dataset/High_Resolution\19101513\S003\L9\E0... 323 635 101 470 19101513 20대
365668 ../Dataset/High_Resolution\19101513\S003\L9\E0... 305 600 101 466 19101513 20대
365669 ../Dataset/High_Resolution\19101513\S003\L9\E0... 282 577 101 467 19101513 20대
365670 ../Dataset/High_Resolution\19101513\S003\L9\E0... 260 557 100 472 19101513 20대
365671 ../Dataset/High_Resolution\19101513\S003\L9\E0... 231 537 96 474 19101513 20대


  • 최종 결과물을 Train시에 사용할 수 있도록 저장합니다.


merged_meta_data.to_csv('meta_data_face_coor_K-Face.csv' , index=False)




2. Train

  • Preprocessing을 거쳐서 얻어진 얼굴 사진 / 연령 / 성별 정보를 이용하여 Model을 Train 시킵니다.



2.1. Prepare Train / Val. Dataset


from numpy.lib.function_base import average
import pandas as pd
import os
import numpy as np
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import tensorflow_addons as tfa
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import GlobalAveragePooling2D , BatchNormalization , Dropout , Dense
from tensorflow.keras.callbacks import TensorBoard , ModelCheckpoint , LearningRateScheduler , EarlyStopping


BATCH_SIZE = 32
DROP_OUT_RATE = 0.2



  • Preprocess 작업에서 얻은 결과물을 읽어옵니다.


dataset_info = pd.read_csv("meta_data_face_coor_K-Face.csv")
dataset_info


file_path left right top bottom ID 연령대 성별
0 ../Dataset/High_Resolution\19062421\S001\L1\E0... 289 491 155 402 19062421 30대
1 ../Dataset/High_Resolution\19062421\S001\L1\E0... 377 581 175 392 19062421 30대
2 ../Dataset/High_Resolution\19062421\S001\L1\E0... 313 502 181 401 19062421 30대
3 ../Dataset/High_Resolution\19062421\S001\L1\E0... 394 602 142 407 19062421 30대
4 ../Dataset/High_Resolution\19062421\S001\L1\E0... 373 589 142 403 19062421 30대
... ... ... ... ... ... ... ... ...
365667 ../Dataset/High_Resolution\19101513\S003\L9\E0... 323 635 101 470 19101513 20대
365668 ../Dataset/High_Resolution\19101513\S003\L9\E0... 305 600 101 466 19101513 20대
365669 ../Dataset/High_Resolution\19101513\S003\L9\E0... 282 577 101 467 19101513 20대
365670 ../Dataset/High_Resolution\19101513\S003\L9\E0... 260 557 100 472 19101513 20대
365671 ../Dataset/High_Resolution\19101513\S003\L9\E0... 231 537 96 474 19101513 20대

365672 rows × 8 columns


  • 성별 / 연령대 값을 One-Hot Encoding하기 위해 Label Encoder를 준비합니다.


gender = dataset_info['성별'].tolist()

le_gender = LabelEncoder()
le_gender.fit(gender)
print(le_gender.classes_)
['남' '여']


age_band = dataset_info['연령대'].tolist()

le_age_band = LabelEncoder()
le_age_band.fit(age_band)
print(le_age_band.classes_)
['20대' '30대' '40대' '50대']



2.2. Split Train / Val. Dataset


  • 이 Model에서 분류하고자 하는 것은 연령대와 성별입니다.

  • 연령대와 성별, 각각을 분류하는 Model을 만들수도 있습니다만, 얼굴 사진에서 Feature를 뽑아내는 과정은 2개의 Model이 동일할 것으로 생각됩니다.

  • 어차피 동일한 Feature를 가지고 2개 Model을 만들기 보다는 Functional API를 사용하여 2개의 Output을 가지는 Model 하나를 만드는 것이 더 효율적일 것 같습니다.

  • 이런 경우에, 연령대 / 성별을 적절한 비율로 가진 Train / Val. Dataset을 분류하는 것이 관건인데, 이를 위해 약간의 Trick을 사용하도록 하겠습니다.

  • 우선 2개의 Label(연령/성별)을 하나로 합쳐서 새로운 Label을 만듭니다. 그러면, 총 8개의 Label( 남,여 x 20,30,40,50 )을 가지는 새로운 Label이 생깁니다.

  • 이 새로운 Label을 가지고 train_test_split으로 분류하면 동일한 비율로 Train / Val. Dataset을 나눌 수 있습니다.

  • 실제 Train시에는 새로운 Label아닌 원래 연령대 / 성별을 이용합니다.


data_file_path = dataset_info[['file_path' , 'left' , 'right' , 'top' , 'bottom' , '연령대' , '성별']]


  • 연령대 / 성별을 합친 새로운 ‘merged_class’ Label을 만듭니다.


dataset_info['merged_class'] = dataset_info['연령대']+dataset_info['성별']
dataset_info['merged_class']


0         30대남
1         30대남
2         30대남
3         30대남
4         30대남
          ... 
365667    20대여
365668    20대여
365669    20대여
365670    20대여
365671    20대여
Name: merged_class, Length: 365672, dtype: object



  • ‘merged_class’은 아래와 같은 비율로 이루어져 있네요.
dataset_info['merged_class'].value_counts()
40대남    80106
30대여    67909
30대남    60042
50대남    52349
40대여    51619
50대여    41387
20대여     7175
20대남     5085
Name: merged_class, dtype: int64



  • ‘merged_class’로 One-Hot으로 나누고, train_test_split으로 동일 비율로 나눕니다.


merged_class = dataset_info['merged_class'].tolist()


le_merged_class = LabelEncoder()
le_merged_class.fit(merged_class)
print(le_merged_class.classes_)
merged_class = le_merged_class.transform(merged_class)
merged_class = tf.keras.utils.to_categorical(merged_class , num_classes=8)
['20대남' '20대여' '30대남' '30대여' '40대남' '40대여' '50대남' '50대여']


file_path_train, file_path_val, y_train, y_val = train_test_split(data_file_path, merged_class, 
                                                                  test_size=0.25, 
                                                                  random_state=777, 
                                                                  stratify = merged_class)


print( len(file_path_train) , len(y_train) , len(file_path_val) , len(y_val) )
274254 274254 91418 91418
file_path_train


file_path left right top bottom 연령대 성별
77525 ../Dataset/High_Resolution\19071841\S002\L21\E... 261 528 111 468 30대
79566 ../Dataset/High_Resolution\19071912\S002\L9\E0... 361 661 104 473 50대
230772 ../Dataset/High_Resolution\19090341\S001\L14\E... 277 557 122 441 40대
273268 ../Dataset/High_Resolution\19091733\S001\L2\E0... 260 534 104 415 50대
219897 ../Dataset/High_Resolution\19083042\S003\L4\E0... 335 612 127 448 50대
... ... ... ... ... ... ... ...
224863 ../Dataset/High_Resolution\19090311\S001\L15\E... 237 532 121 452 30대
179967 ../Dataset/High_Resolution\19082142\S002\L24\E... 257 527 101 439 30대
85366 ../Dataset/High_Resolution\19072221\S003\L4\E0... 238 520 90 455 40대
180009 ../Dataset/High_Resolution\19082142\S002\L29\E... 274 528 89 469 30대
55958 ../Dataset/High_Resolution\19071242\S003\L1\E0... 320 603 116 453 40대

274254 rows × 7 columns


  • 이제 나누어진 Train / Val.로 tf.Dataset을 만듭니다.


train_left = file_path_train['left'].tolist()
train_right = file_path_train['right'].tolist()
train_top = file_path_train['top'].tolist()
train_bottom = file_path_train['bottom'].tolist()
train_file_path = file_path_train['file_path'].tolist()
age_band = file_path_train['연령대'].tolist()

age_band = le_age_band.transform(age_band)
train_age_band = tf.keras.utils.to_categorical(age_band , num_classes=4)
train_age_band


array([[0., 1., 0., 0.],
       [0., 0., 0., 1.],
       [0., 0., 1., 0.],
       ...,
       [0., 0., 1., 0.],
       [0., 1., 0., 0.],
       [0., 0., 1., 0.]], dtype=float32)


gender = file_path_train['성별'].tolist()

gender = le_gender.transform(gender)
train_gender = tf.keras.utils.to_categorical(gender , num_classes=2)
train_gender


array([[0., 1.],
       [0., 1.],
       [0., 1.],
       ...,
       [0., 1.],
       [0., 1.],
       [0., 1.]], dtype=float32)



val_left = file_path_val['left'].tolist()
val_right = file_path_val['right'].tolist()
val_top = file_path_val['top'].tolist()
val_bottom = file_path_val['bottom'].tolist()
val_file_path = file_path_val['file_path'].tolist()


age_band = file_path_val['연령대'].tolist()

age_band = le_age_band.transform(age_band)
val_age_band = tf.keras.utils.to_categorical(age_band , num_classes=4)
val_age_band


array([[0., 1., 0., 0.],
       [0., 0., 0., 1.],
       [0., 0., 1., 0.],
       ...,
       [0., 1., 0., 0.],
       [0., 0., 1., 0.],
       [0., 1., 0., 0.]], dtype=float32)


gender = file_path_val['성별'].tolist()

gender = le_gender.transform(gender)
val_gender = tf.keras.utils.to_categorical(gender , num_classes=2)
val_gender


array([[0., 1.],
       [1., 0.],
       [0., 1.],
       ...,
       [0., 1.],
       [1., 0.],
       [0., 1.]], dtype=float32)



  • Map function입니다.

  • Image를 읽어서 얼굴 부분만을 잘라내서 ResNet Input Format으로 변환합니다.

  • 또 하나 주목할 점은 Train시킬 Model은 2개의 Label을 학습해야 하므로 (label_gender , label_age_band) 와 같이 2개의 Label을 Return합니다.

def load_image( image_path , left , right , top , bottom , label_age_band , label_gender ):
    img = tf.io.read_file(image_path)
    img = tf.image.decode_jpeg(img, channels=3)   
    img = tf.image.crop_to_bounding_box( img , top , left, bottom - top , right - left )
    
    img = tf.image.resize(img, (224, 224))
    img = tf.keras.applications.resnet50.preprocess_input(img)
    
    return img , (label_gender , label_age_band)
train_dataset = tf.data.Dataset.from_tensor_slices( (train_file_path , 
                                                     train_left , 
                                                     train_right , 
                                                     train_top , 
                                                     train_bottom , 
                                                     train_age_band,
                                                     train_gender) )
val_dataset = tf.data.Dataset.from_tensor_slices( (val_file_path , 
                                                   val_left , 
                                                   val_right , 
                                                   val_top , 
                                                   val_bottom ,
                                                   val_age_band,
                                                   val_gender) )
train_dataset = train_dataset.shuffle(buffer_size=len(train_file_path))\
                                        .map( load_image, num_parallel_calls=tf.data.experimental.AUTOTUNE)\
                                        .batch(BATCH_SIZE)\
                                        .prefetch(tf.data.experimental.AUTOTUNE)
val_dataset = val_dataset.shuffle(buffer_size=len(val_file_path))\
                            .map( load_image, num_parallel_calls=tf.data.experimental.AUTOTUNE)\
                            .batch(BATCH_SIZE)\
                            .prefetch(tf.data.experimental.AUTOTUNE)



2.3. Build Model


  • 2개의 Label에 대한 학습을 해야하기 때문에 Sequential Model이 아닌 Functional API를 사용한 Model을 만들도록 하겠습니다.

  • Input은 ResNet Input Format에 맞게 만듭니다.

input = tf.keras.Input(shape=(224, 224, 3), name="input")


  • Feature Extractor로 ResNet을 사용하도록 하겠습니다.
r = tf.keras.applications.resnet.ResNet50(
    weights=None,
    input_shape=(224, 224, 3),
    include_top=False)(input)


  • 그 이후에 일반적인 Dense Layer들을 추가합니다.
g = GlobalAveragePooling2D()(r)
g = Dropout(DROP_OUT_RATE)(g)
g = BatchNormalization()(g)
g = Dense(128,activation="relu")(g)
g = Dropout(DROP_OUT_RATE)(g)
g = BatchNormalization()(g)


  • 마지막으로, 2개의 출력에 대한 Dense Layer도 추가합니다.


output_gender = Dense(2, activation='softmax' , name="output_gender")(g)
output_age_band = Dense(4, activation='softmax' , name="output_age_band")(g)


  • 위에서 만든 Input / Output들로 Model을 만듭니다.
model = tf.keras.Model(
    inputs=input,
    outputs=[output_gender, output_age_band],
)
model.summary()
Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input (InputLayer)              [(None, 224, 224, 3) 0                                            
__________________________________________________________________________________________________
resnet50 (Functional)           (None, 7, 7, 2048)   23587712    input[0][0]                      
__________________________________________________________________________________________________
global_average_pooling2d (Globa (None, 2048)         0           resnet50[0][0]                   
__________________________________________________________________________________________________
dropout (Dropout)               (None, 2048)         0           global_average_pooling2d[0][0]   
__________________________________________________________________________________________________
batch_normalization (BatchNorma (None, 2048)         8192        dropout[0][0]                    
__________________________________________________________________________________________________
dense (Dense)                   (None, 128)          262272      batch_normalization[0][0]        
__________________________________________________________________________________________________
dropout_1 (Dropout)             (None, 128)          0           dense[0][0]                      
__________________________________________________________________________________________________
batch_normalization_1 (BatchNor (None, 128)          512         dropout_1[0][0]                  
__________________________________________________________________________________________________
output_gender (Dense)           (None, 2)            258         batch_normalization_1[0][0]      
__________________________________________________________________________________________________
output_age_band (Dense)         (None, 4)            516         batch_normalization_1[0][0]      
==================================================================================================
Total params: 23,859,462
Trainable params: 23,801,990
Non-trainable params: 57,472
__________________________________________________________________________________________________



2.4. Callbacks


  • 필요한 Callback들을 정의합니다.
initial_learning_rate = 0.01

def lr_exp_decay(epoch, lr):
    k = 0.1
    return initial_learning_rate * np.math.exp(-k*epoch)

lr_scheduler = LearningRateScheduler(lr_exp_decay, verbose=1)
log_dir = os.path.join('Logs')
CHECKPOINT_PATH = os.path.join('CheckPoints_K-Face_Gender_Age_Band_F1_Score')
tb_callback = TensorBoard(log_dir=log_dir)
cp = ModelCheckpoint(filepath=CHECKPOINT_PATH, 
                     monitor='val_output_age_band_accuracy',
                     #monitor='val_F1_metric',
                     save_best_only = True,
                     verbose = 1)
es = EarlyStopping(monitor = 'val_output_age_band_loss', patience = 2, mode = 'auto')
F1_metric = tfa.metrics.F1Score(num_classes=2 , average=None)



  • Model Compile시에 2개의 Output에 각각 Loss / Metric을 지정해 줄 수 있습니다.
model.compile(
    optimizer=tf.keras.optimizers.Adam(1e-3),    
    
    loss={'output_gender':'binary_crossentropy',
          'output_age_band':'categorical_crossentropy'},    
    
    metrics={'output_gender':'accuracy' , 
             'output_age_band' : 'accuracy'}
)


  • 이제 모든 준비가 끝났으니 Train을 시작합니다.


hist = model.fit(train_dataset,
                 validation_data=val_dataset,
                 callbacks=[lr_scheduler , cp , tb_callback , es],
                 epochs = 10,
                 verbose = 1
                )
C:\Users\Moon\anaconda3\envs\TF.2.5.0-GPU\lib\site-packages\tensorflow\python\keras\utils\generic_utils.py:494: CustomMaskWarning: Custom mask layers require a config and must override get_config. When loading, the custom mask layer must be passed to the custom_objects argument.
  warnings.warn('Custom mask layers require a config and must override '


Epoch 1/10

Epoch 00001: LearningRateScheduler reducing learning rate to 0.01.
8571/8571 [==============================] - 1378s 160ms/step - loss: 1.2292 - output_gender_loss: 0.2243 - output_age_band_loss: 1.0049 - output_gender_accuracy: 0.8933 - output_age_band_accuracy: 0.5337 - val_loss: 1.0698 - val_output_gender_loss: 0.1136 - val_output_age_band_loss: 0.9562 - val_output_gender_accuracy: 0.9571 - val_output_age_band_accuracy: 0.6229

Epoch 00001: val_output_age_band_accuracy improved from -inf to 0.62286, saving model to CheckPoints_K-Face_Gender_Age_Band_F1_Score


C:\Users\Moon\anaconda3\envs\TF.2.5.0-GPU\lib\site-packages\tensorflow\python\keras\utils\generic_utils.py:494: CustomMaskWarning: Custom mask layers require a config and must override get_config. When loading, the custom mask layer must be passed to the custom_objects argument.
  warnings.warn('Custom mask layers require a config and must override '


INFO:tensorflow:Assets written to: CheckPoints_K-Face_Gender_Age_Band_F1_Score\assets
Epoch 2/10

Epoch 00002: LearningRateScheduler reducing learning rate to 0.009048374180359595.
8571/8571 [==============================] - 1384s 161ms/step - loss: 0.5148 - output_gender_loss: 0.0518 - output_age_band_loss: 0.4629 - output_gender_accuracy: 0.9817 - output_age_band_accuracy: 0.8172 - val_loss: 0.4605 - val_output_gender_loss: 0.0664 - val_output_age_band_loss: 0.3941 - val_output_gender_accuracy: 0.9782 - val_output_age_band_accuracy: 0.8418

Epoch 00002: val_output_age_band_accuracy improved from 0.62286 to 0.84179, saving model to CheckPoints_K-Face_Gender_Age_Band_F1_Score


C:\Users\Moon\anaconda3\envs\TF.2.5.0-GPU\lib\site-packages\tensorflow\python\keras\utils\generic_utils.py:494: CustomMaskWarning: Custom mask layers require a config and must override get_config. When loading, the custom mask layer must be passed to the custom_objects argument.
  warnings.warn('Custom mask layers require a config and must override '


INFO:tensorflow:Assets written to: CheckPoints_K-Face_Gender_Age_Band_F1_Score\assets
Epoch 3/10

Epoch 00003: LearningRateScheduler reducing learning rate to 0.008187307530779819.
8571/8571 [==============================] - 1382s 161ms/step - loss: 0.2292 - output_gender_loss: 0.0267 - output_age_band_loss: 0.2025 - output_gender_accuracy: 0.9906 - output_age_band_accuracy: 0.9262 - val_loss: 0.6303 - val_output_gender_loss: 0.0726 - val_output_age_band_loss: 0.5578 - val_output_gender_accuracy: 0.9743 - val_output_age_band_accuracy: 0.8105

Epoch 00003: val_output_age_band_accuracy did not improve from 0.84179
Epoch 4/10

Epoch 00004: LearningRateScheduler reducing learning rate to 0.007408182206817179.
8571/8571 [==============================] - 1385s 162ms/step - loss: 0.1404 - output_gender_loss: 0.0188 - output_age_band_loss: 0.1216 - output_gender_accuracy: 0.9932 - output_age_band_accuracy: 0.9568 - val_loss: 0.3266 - val_output_gender_loss: 0.0297 - val_output_age_band_loss: 0.2969 - val_output_gender_accuracy: 0.9890 - val_output_age_band_accuracy: 0.8906

Epoch 00004: val_output_age_band_accuracy improved from 0.84179 to 0.89063, saving model to CheckPoints_K-Face_Gender_Age_Band_F1_Score


C:\Users\Moon\anaconda3\envs\TF.2.5.0-GPU\lib\site-packages\tensorflow\python\keras\utils\generic_utils.py:494: CustomMaskWarning: Custom mask layers require a config and must override get_config. When loading, the custom mask layer must be passed to the custom_objects argument.
  warnings.warn('Custom mask layers require a config and must override '


INFO:tensorflow:Assets written to: CheckPoints_K-Face_Gender_Age_Band_F1_Score\assets
Epoch 5/10

Epoch 00005: LearningRateScheduler reducing learning rate to 0.006703200460356393.
8571/8571 [==============================] - 1373s 160ms/step - loss: 0.0986 - output_gender_loss: 0.0146 - output_age_band_loss: 0.0840 - output_gender_accuracy: 0.9946 - output_age_band_accuracy: 0.9700 - val_loss: 1.0131 - val_output_gender_loss: 0.0804 - val_output_age_band_loss: 0.9327 - val_output_gender_accuracy: 0.9738 - val_output_age_band_accuracy: 0.7865

Epoch 00005: val_output_age_band_accuracy did not improve from 0.89063
Epoch 6/10

Epoch 00006: LearningRateScheduler reducing learning rate to 0.006065306597126334.
8571/8571 [==============================] - 1371s 160ms/step - loss: 0.0751 - output_gender_loss: 0.0121 - output_age_band_loss: 0.0630 - output_gender_accuracy: 0.9954 - output_age_band_accuracy: 0.9778 - val_loss: 0.1992 - val_output_gender_loss: 0.0330 - val_output_age_band_loss: 0.1662 - val_output_gender_accuracy: 0.9879 - val_output_age_band_accuracy: 0.9432

Epoch 00006: val_output_age_band_accuracy improved from 0.89063 to 0.94324, saving model to CheckPoints_K-Face_Gender_Age_Band_F1_Score


C:\Users\Moon\anaconda3\envs\TF.2.5.0-GPU\lib\site-packages\tensorflow\python\keras\utils\generic_utils.py:494: CustomMaskWarning: Custom mask layers require a config and must override get_config. When loading, the custom mask layer must be passed to the custom_objects argument.
  warnings.warn('Custom mask layers require a config and must override '


INFO:tensorflow:Assets written to: CheckPoints_K-Face_Gender_Age_Band_F1_Score\assets
Epoch 7/10

Epoch 00007: LearningRateScheduler reducing learning rate to 0.005488116360940264.
8571/8571 [==============================] - 1369s 160ms/step - loss: 0.0591 - output_gender_loss: 0.0105 - output_age_band_loss: 0.0485 - output_gender_accuracy: 0.9959 - output_age_band_accuracy: 0.9828 - val_loss: 1.2796 - val_output_gender_loss: 0.0693 - val_output_age_band_loss: 1.2103 - val_output_gender_accuracy: 0.9817 - val_output_age_band_accuracy: 0.8210

Epoch 00007: val_output_age_band_accuracy did not improve from 0.94324
Epoch 8/10

Epoch 00008: LearningRateScheduler reducing learning rate to 0.004965853037914095.
8571/8571 [==============================] - 1372s 160ms/step - loss: 0.0468 - output_gender_loss: 0.0089 - output_age_band_loss: 0.0380 - output_gender_accuracy: 0.9964 - output_age_band_accuracy: 0.9865 - val_loss: 1.4768 - val_output_gender_loss: 0.2161 - val_output_age_band_loss: 1.2607 - val_output_gender_accuracy: 0.9596 - val_output_age_band_accuracy: 0.8651

Epoch 00008: val_output_age_band_accuracy did not improve from 0.94324



plt.plot(hist.history['output_gender_accuracy'])
plt.plot(hist.history['val_output_gender_accuracy'])
plt.title('Gender Model accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend(['Train', 'Val'], loc='upper left')
plt.show()

plt.plot(hist.history['output_age_band_accuracy'])
plt.plot(hist.history['val_output_age_band_accuracy'])
plt.title('Age Band Model accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend(['Train', 'Val'], loc='upper left')
plt.show()

plt.plot(hist.history['loss'])
plt.plot(hist.history['val_loss'])
plt.title('Model loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend(['Train', 'Val'], loc='upper left')
plt.show()







3. Inference


  • 학습한 Model이 잘 작동하는지 확인해 보겠습니다.

  • Model에 Cam. Image를 Input할 때는 Train에 입력한 것과 동일한 Shape으로 Input해야 합니다.

  • 그러므로, Preprocess때와 동일하게 Cam. Image에서 얼굴부분을 Detect해서 변환 후 Input으로 넣어야 합니다.


import cv2
import numpy as np
import tensorflow as tf
from keras.models import load_model, save_model
MODEL_FILE = "opencv_face_detector_uint8.pb"
CONFIG_FILE = "opencv_face_detector.pbtxt"
SIZE = 300
CONFIDENCE_FACE = 0.9



  • 결과를 출력하기 위한 값입니다.
AGE_BAND_RESULT = ["20~30","30~40","40~50","Over 50"]
GENDER_RESULT = ["Man","Woman"]



  • Preprocess에서 사용한 DNN Face Detector를 그대로 사용하도록 하겠습니다.


# Load Face Detection Model
net = cv2.dnn.readNetFromTensorflow( MODEL_FILE , CONFIG_FILE )



  • Train을 마친 Model을 Load합니다.


print("Loading Model...")

model = load_model("CheckPoints_K-Face_Gender_Age_Band_F1_Score")
Loading Model...


  • Cam을 연결해야 합니다.
cap = cv2.VideoCapture(0)


  • 실시간으로 얼굴 부분을 Detect해서 분류합니다.

  • DNN Face Detector는 실시간으로 처리할 수 있을 만큼 빠릅니다.



  • Code 설명

    if score > CONFIDENCE_FACE:

    • Face Detector가 Detector한 결과가 일정 신뢰도 이상인 경우에만 처리합니다.

    left = int(detection[3] * cols)
    top = int(detection[4] * rows)
    right = int(detection[5] * cols)
    bottom = int(detection[6] * rows)

    cropped = frame[top:bottom, left:right]
    cropped = cv2.cvtColor(cropped, cv2.COLOR_BGR2RGB)
    cropped = cv2.resize( cropped , dsize=(224,224) )
    cropped = np.array(cropped).reshape(-1,224,224,3)

    - 얼굴 부분만 잘라내서 Model Input에 넣을 수 있도록 Reshape합니다.

    pred = model.predict( cropped )
    gender_result = pred[0]
    age_band_result = pred[1]

    • Model의 결과물에서 성별과 연령대를 구분해서 출력해 줍니다.


while cv2.waitKey(1) < 0:
    ret, frame = cap.read()
    rows, cols, channels = frame.shape

    blob = cv2.dnn.blobFromImage(frame, 1.0, (SIZE, SIZE))

    net.setInput(blob)
    detections = net.forward()

    for detection in detections[0, 0]:

        score = float(detection[2])

        if score > CONFIDENCE_FACE:

            if detection[3] >= 1.00 or detection[4] >= 1.00 or detection[5] >= 1.00 or detection[6] >= 1.00 or detection[3] <= 0 or detection[4] < 0 or detection[5] <= 0 or detection[6] <= 0:
                pass
            else:
                left = int(detection[3] * cols)
                top = int(detection[4] * rows)
                right = int(detection[5] * cols)
                bottom = int(detection[6] * rows)

                cropped = frame[top:bottom, left:right]
                cropped = cv2.cvtColor(cropped, cv2.COLOR_BGR2RGB)
                cropped = cv2.resize( cropped , dsize=(224,224) )
                cropped = np.array(cropped).reshape(-1,224,224,3)

                pred = model.predict( cropped )
                
                gender_result = pred[0]
                age_band_result = pred[1]

                AGE_BAND_RESULT = ["20~30","30~40","40~50","Over 50"]
                GENDER_RESULT = ["Man","Woman"]
                
                Result = "{0} , {1}".format(GENDER_RESULT[int(np.argmax(np.reshape( gender_result , (1,-1) )))] , AGE_BAND_RESULT[int(np.argmax(np.reshape( age_band_result , (1,-1) )))])

                cv2.putText(frame, Result, (left, top - 10), cv2.FONT_HERSHEY_SIMPLEX,0.5, (0, 255, 0), 2)
                cv2.rectangle(frame, (left, top), (right, bottom), (0, 255, 0), 2)


    cv2.imshow("VideoFrame", frame)

cap.release()
cv2.destroyAllWindows()