Apparent Age and Gender Prediction in Keras
ETH Zurich University(Switzerland)의 컴퓨터 비젼 연구자들은 매우 성공적인 겉보기 연령과 나이 예측 모델을 발표했다. 둘 모두 전이학습(transfer learning)을 위해 그들이 머신러닝 모델을 어떻게 설계하였는지와 사전 훈련된 가중치를 공유했다. 이들의 구현은 Caffe 프레임워크 기반이다. Caffe 모델을 Keras/Tensorflow 로 변환을 시도했지만 실패했다. 그렇기 때문에 케라스로 설계부터 이 연구를 도입해 보려고 한다.
Katy Perry Transformation
이 글이 제공하는 것은?
실시간에서 나이와 성별 예측을 적용할 수 있다.
Pre-trained model
이 글에서는 처음부터 나이와 성별 예측 모델을 재훈련한다. 만약 오직 예측 단계에만 관심있다면 아래 비디오가 마음에 들 것이다. 이 주제는 실제 Age and Gender Prediction with Deep Learning in OpenCV 글에서 다뤄진다. 이런 경우에는 OpenCV안에 Caffe를 위해 사전훈련된 모델을 사용한다. 게다가 여러분은 Caffe환경을 구성할 필요가 없다. OpenCV는 dnn 모듈내에 Caffe 모델을 구성하는 것을 포함한다.
다른 한편으로 훈련단계에 관심이 있다면 이 글을 계속 읽으면 된다.
Dataset
원래 연구는 IMDB(7GB)와 위키디피아(1GB)로부터 수집된 얼굴 사진을 사용했다. 이 데이터셋은 여기에서 찾을 수 있다. 이 글에서는 빠르게 솔루션 개발을 위해 위키 데이터소스만을 사용한다.
'wiki_crop.tar'는 100개의 폴더와 index파일을 생성한다.(index.mat) 인덱스 파일은 Matlab 포멧으로 저장되어 있다. SciPy로 파이썬에서 Matlab 파일을 읽을 수 있다.
import scipy.io
mat = scipy.io.loadmat('wiki_crop/wiki.mat')
판다스 프레임워크로 변환하면 더 쉽게 변환할 수 있다.
instances = mat['wiki'][0][0][0].shape[1]
columns = ["dob", "photo_taken", "full_path", "gender", "name", "face_location", "face_score", "second_face_score"]
import pandas as pd
df = pd.DataFrame(index = range(0,instances), columns = columns)
for i in mat:
if i == "wiki":
current_array = mat[i][0][0]
for j in range(len(current_array)):
df[columns[j]] = pd.DataFrame(current_array[j][0])
Initial data set
데이터셋은 Matlab datenum 포멧으로 date_of_birth(dob)를 포함한다. 이를 파이썬 데이터프레임으로 변환해야 한다. 우리는 단지 태어난 년도까지만 필요하다.
from datetime import datetime, timedelta
def datenum_to_datetime(datenum):
days = datenum % 1
hours = days % 1 * 24
minutes = hours % 1 * 60
seconds = minutes % 1 * 60
exact_date = datetime.fromordinal(int(datenum)) \
+ timedelta(days=int(days)) + timedelta(hours=int(hours)) \
+ timedelta(minutes=int(minutes)) + timedelta(seconds=round(seconds)) \
- timedelta(days=366)
return exact_date.year
df['date_of_birth'] = df['dob'].apply(datenum_to_datetime)
Adding exact birth date
Matlob datenum 포멧에서 태어난 년도를 추출한다.
이제 태어난 년도와 사진이 찍힌 시간을 얻었다. 이들 값을 서로 빼면 나이를 얻을 수 있다.
df['age'] = df['photo_taken'] - df['date_of_birth']
Data cleaning
위키 데이터셋에서 몇몇 사진은 사람이 없다. 예를 들면, 꽃병(vase) 사진이 데이터셋에 존재한다. 게다가 몇몇 사진은 두명을 포함하기도 한다. 또한 몇몇은 떨어져 있다. 얼굴 점수(face score) 값은 사진이 명확한지 아닌지를 이해하는데 도움이 된다. 또한 나이 정보는 몇몇 레코드에서 빠졌다. 이러한 것 모두가 모델을 혼란스럽게 만든다. 우리는 이러한 것들을 무시해야 한다. 마지막으로 필요없는 컬럼은 메모리 소모를 줄이기 위해 빼야한다.
#remove pictures does not include face
df = df[df['face_score'] != -np.inf]
#some pictures include more than one face, remove them
df = df[df['second_face_score'].isna()]
#check threshold
df = df[df['face_score'] >= 3]
#some records do not have a gender information
df = df[~df['gender'].isna()]
df = df.drop(columns = ['name','face_score','second_face_score','date_of_birth','face_location'])
몇몇 사진은 태어나지 않은 사람이다. 나이값이 몇몇 레코드에서는 음(-)의 값이다. 더러운 데이커가 이를 유발할 수 있다. 게다가 몇몇은 100살 이상까지 살았다. 우리는 0에서 100살로 나이예측 문제를 제한해야 한다.
#some guys seem to be greater than 100. some of these are paintings. remove these old guys
df = df[df['age'] <= 100]
#some guys seem to be unborn in the data set
df = df[df['age'] > 0]
원시 데이터는 아래 데이터프레임처럼 보일 것이다.
Raw data set
목표 레이블 분포를 사각화해 보자.
histogram_age = df['age'].hist(bins=df['age'].nunique())
histogram_gender = df['gender'].hist(bins=df['gender'].nunique())
Age and gender distribution in the data set
'full_path' 컬럼은 디스크에서 사진의 위치를 나타낸다. 우리는 사진의 필셀값이 필요하다.
target_size = (224, 224)
def getImagePixels(image_path):
img = image.load_img("wiki_crop/%s" % image_path[0], grayscale=False, target_size=target_size)
x = image.img_to_array(img).reshape(1, -1)[0]
#x = preprocess_input(x)
return x
df['pixels'] = df['full_path'].apply(getImagePixels)
사진의 실제 픽셀값을 추출할 수 있다.
Adding pixels
Apparent age prediction model
나이 예측은 회귀(regression) 문제이다. 하지만 연구자들은 이를 분류(classification) 문제로 정의하였다. 0세에서 100세에 대한 출력레이어로 101개 분류가 있다. 연구자들은 이 역할을 위해 전이학습(transfer learning)을 적용하였다. 그들은 이미지넷(imagenet)을 위한 VGG를 선택하였다.
Preparing input output
판다스 데이터프레임은 나이와 성별 예측 작업을 위한 입력과 출력 정보 모두를 포함한다. 우리는 오직 나이 작업에만 집중해야 한다.
classes = 101 #0 to 100
target = df['age'].values
target_classes = keras.utils.to_categorical(target, classes)
features = []
for i in range(0, df.shape[0]):
features.append(df['pixels'].values[i])
features = np.array(features)
features = features.reshape(features.shape[0], 224, 224, 3)
또한 훈련과 테스트 셋으로 데이터셋을 나눠야 한다.
from sklearn.model_selection import train_test_split
train_x, test_x, train_y, test_y = train_test_split(features, target_classes, test_size=0.30)
최종 데이터셋은 22,578개의 인스턴스로 구성되며 이를 15,905개의 훈련 인스턴스와 6,673개의 테스트 인스턴스로 분할한다.
Transfer learning
앞서 언급했듯 연구자는 VGG 이미지넷 모델을 사용했고 이 데어터셋에 대해 가중치를 세부조정하였다. 여기서도 vGG-Face model을 사용한다. 왜냐하면 이 모델이 얼굴인식 작업에 대해 세부조정 되었기 때문이다. 이런 방법으로 사람 얼굴에서 패턴에 대한 결과를 얻을 수 있다.
#VGG-Face model
model = Sequential()
model.add(ZeroPadding2D((1,1),input_shape=(224,224, 3)))
model.add(Convolution2D(64, (3, 3), activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D((2,2), strides=(2,2)))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(128, (3, 3), activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(128, (3, 3), activation='relu'))
model.add(MaxPooling2D((2,2), strides=(2,2)))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(256, (3, 3), activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(256, (3, 3), activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(256, (3, 3), activation='relu'))
model.add(MaxPooling2D((2,2), strides=(2,2)))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, (3, 3), activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, (3, 3), activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, (3, 3), activation='relu'))
model.add(MaxPooling2D((2,2), strides=(2,2)))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, (3, 3), activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, (3, 3), activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, (3, 3), activation='relu'))
model.add(MaxPooling2D((2,2), strides=(2,2)))
model.add(Convolution2D(4096, (7, 7), activation='relu'))
model.add(Dropout(0.5))
model.add(Convolution2D(4096, (1, 1), activation='relu'))
model.add(Dropout(0.5))
model.add(Convolution2D(2622, (1, 1)))
model.add(Flatten())
model.add(Activation('softmax'))
VGG-Face 모델에 대한 사전훈련된 가중치를 로드한다. 이 가중치는 여기에서 찾을 수 있다.
#pre-trained weights of vgg-face model.
#you can find it here: https://drive.google.com/file/d/1CPSeum3HpopfomUEK1gybeuIVoeJT_Eo/view?usp=sharing
#related blog post: https://sefiks.com/2018/08/06/deep-face-recognition-with-keras/
model.load_weights('vgg_face_weights.h5')
모델의 앞단 레이어가 이미 몇가지 패턴을 탐지하기 때문에 우리는 이 앞단 레이어의 가중치를 고정(lock)해야 한다. 기본부터 네트워크를 학습하는 것은 이런 중요정보 손실을 유할 할 수 있다. (저자의 경우) 마지막 3개의 합성곱(convolution) 레이어를 제외한 모든 레이어를 고정했다.(마지막 7개의 model.add 유닛을 예외로 만들었다.) 또한 마지막 합성곱 레이어가 2,622개의 유닛을 갖기 때문에 잘랐다. 여기서는 나이 예측 작업체 단지 101개(나이는 0세에서 100세까지) 유닛만이 필요하다. 그래서 101개의 유닛으로 구성되는 맞춤 합성곱 레이어를 추가한다.
for layer in model.layers[:-7]:
layer.trainable = False
base_model_output = Sequential()
base_model_output = Convolution2D(101, (1, 1), name='predictions')(model.layers[-4].output)
base_model_output = Flatten()(base_model_output)
base_model_output = Activation('softmax')(base_model_output)
age_model = Model(inputs=model.input, outputs=base_model_output)
Training
이 작업은 다중(multi-class) 분류 문제이다. 손실함수(loss function)은 categorical crossentropy이어야 한다. 최적화(optimization) 알고리즘은 더 빠르게 손실로 수렴하기 위해 Adam을 사용한다. 지나친 반복을 모니터링하고 오버피팅을 피하기 위해 checkpoint를 생성한다. 최소 검증 손실값(minimum validation loss)을 갖는 반복은 최고의 가중치를 포함할 것이다. 그렇기 때문에 검증 손실을 모니터링하고 가장 좋은 것 하나만을 저장한다.
오버피팅을 피하기 위해 각 에포크에 임의의 256개의 인스턴스를 전달한다.
age_model.compile(loss='categorical_crossentropy', optimizer=keras.optimizers.Adam(), metrics=['accuracy'])
checkpointer = ModelCheckpoint(filepath='age_model.hdf5'
, monitor = "val_loss", verbose=1, save_best_only=True, mode = 'auto')
scores = []
epochs = 250; batch_size = 256
for i in range(epochs):
print("epoch ",i)
ix_train = np.random.choice(train_x.shape[0], size=batch_size)
score = age_model.fit(train_x[ix_train], train_y[ix_train], epochs=1, validation_data=(test_x, test_y), callbacks=[checkpointer])
scores.append(score)
검증 손실이 최소가 되는 것 같고 에포크가 증가하면 오버피팅을 발생시킨다.
Loss for age prediction task
Model evaluation on test set
여러분은 테스트셋에서 최종 모델을 평가할 수 있다.
age_model.evaluate(test_x, test_y, verbose=1)
위 코드는 6,673개의 테스트 인스턴스에 대해 각각 검증 손실과 정확도 두가지를 모두 보여주며 아래와 같은 결과를 보였다.
[2.871919590848929, 0.24298789490543357]
24%의 정확도는 매우 낮아 보인다. 사실은 그렇지도 않다. 여기서 연기자들은 나이 예측 접근방법을 개발하고 분류작업을 회귀로 바꾸었다. 그들은 여러분이 각 소프트맥스(softmax) 출력과 해당 레이블을 곱해야한다고 한다. 이 곱셈들을 합산하면 겉보기 나이 예측이 된다.
Age prediction approach
이 작업은 파이썬 넘파이로 매우 쉽게 한다.
predictions = age_model.predict(test_x)
output_indexes = np.array([i for i in range(0, 101)])
apparent_predictions = np.sum(predictions * output_indexes, axis = 1)
여기서 MAE(Mean Absolute Error) 지표(metric)는 시스템을 평가하기 위해 좀 더 의미가 있다.
mae = 0
for i in range(0 ,apparent_predictions.shape[0]):
prediction = int(apparent_predictions[i])
actual = np.argmax(test_y[i])
abs_error = abs(prediction - actual)
actual_mean = actual_mean + actual
mae = mae + abs_error
mae = mae / apparent_predictions.shape[0]
print("mae: ",mae)
print("instances: ",apparent_predictions.shape[0])
우리의 겉보기 나이 에측 모델은 평균적으로 $\pm4.65$ 오차로 예측한다.
Testing model on custom images
모델에 custom 이미지를 전달할 때 모델의 힘을 느낄 수 있다.
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
def loadImage(filepath):
test_img = image.load_img(filepath, target_size=(224, 224))
test_img = image.img_to_array(test_img)
test_img = np.expand_dims(test_img, axis = 0)
test_img /= 255
return test_img
picture = "marlon-brando.jpg"
prediction = age_model.predict(loadImage(picture))
예측 변수는 각 나이 분류에 대한 분포를 저장한다. 이를 모니터링하는 것도 흥미롭다.
y_pos = np.arange(101)
plt.bar(y_pos, prediction[0], align='center', alpha=0.3)
plt.ylabel('percentage')
plt.title('age')
plt.show()
아래 분포는 Godfather의 Marion Barndo의 나이 예측 분포이다. 지배적인 나이가 44세이지만 가중치가 적용된 나이는 1972년 그의 정확한 나이인 48세이다.
Age prediction distribution for Marlon Brando in Godfather
이 나이 분포에서 겉보기 나이를 계산할 수 있다.
img = image.load_img(picture)
plt.imshow(img)
plt.show()
print("most dominant age class (not apparent age): ",np.argmax(prediction))
apparent_age = np.round(np.sum(prediction * output_indexes, axis = 1))
print("apparent age: ", int(apparent_age[0]))
비록 좋은 면(good perspective)이 없지만 결과는 만족스럽다. Godfather part1에서 Marion Brando는 48세이고 Pacino는 32세였다.
Apparent Age Prediction in Godfather
Compare to original study
이미 언급한 것처럼 원래 연구가 주로 Caffe기반이고 우리는 Keras를 위한 사전 훈련된 가중치가 필요하기 때문에 기본 모델을 재훈련하였다. 원래 연구는 Apparent age V1 (ICCV ’15)에 대한 ChaLearn Looking at People (LAP) challenge의 우승자였다.
우리가 누군가의 나이를 예측하기를 바랬고 실제 나이 대신 그/그녀의 나이에 대한 몇가지 예측이 있다. 따라서 여러분의 예측은 배심원 예측을 평균하고 표준편차를 구하여 평가된다.
Evaluation formula
만약 예측이 예측의 평균과 같다면 오류는 0이 된다. 게다가 예측이 예측에 평균에 가깝지 않지만 배심원 예측의 표준편차기 높다면 오류가 0에 가깝다. 다른 한편으로 예측이 예측의 평균에 가깝지 않고 배심원 예측의 표준편차가 낮으면 벌칙이 주어진다.
from math import e
df['epsilon'] = e ** ( -1*( (df['prediction'] - df['mean_age']) ** 2 ) / (2*(df['std_age']**2)) )
df['epsilon'].mean()
이 모델의 $\epsilon$값은 0.387378이고 1,079개의 인스턴스에 대한 MAE는 7.887859이다. 다른 한편으로 원래 연구의 $\epsilon$값은 0.264975였다. 그들은 $\epsilon$의 사람 참조를 0.34로 선언했다. 따라서 원래 연구는 이 글에서 만든 모델 보다 약간 더 정확하다. 게다가 이 글의 모델은 나이 예측에 대해 사람 주준에 가깝다.
평가 데이터셋과 레이블은 여기에서 찾을 수 있다.
Face detection
얼굴 탐지에 관해서는 이전 글들을 참조하자.
Gender prediction model
겉보기 나이 예측은 도전중인 문제이다. 그러나 성별 예측은 훨씬 더 예측 가능하다.
이진 인코딩을 목표 성별 클래스에 적용한다.
target = df['gender'].values
target_classes = keras.utils.to_categorical(target, 2)
그리고 남자와 여자에 대한 출력 레이어에 2개의 클래스를 놓는다.
for layer in model.layers[:-7]:
layer.trainable = False
base_model_output = Sequential()
base_model_output = Convolution2D(2, (1, 1), name='predictions')(model.layers[-4].output)
base_model_output = Flatten()(base_model_output)
base_model_output = Activation('softmax')(base_model_output)
gender_model = Model(inputs=model.input, outputs=base_model_output)
이제 모델을 훈련할 준비가 되었다.
scores = []
epochs = 250; batch_size = 256
for i in range(epochs):
print("epoch ",i)
ix_train = np.random.choice(train_x.shape[0], size=batch_size)
score = gender_model.fit(train_x[ix_train], train_y[ix_train], epochs=1, validation_data=(test_x, test_y), callbacks=[checkpointer])
scores.append(score)
모델이 수렴된 것같아 보인다. 훈련을 끝내는 것이 좋다.
Loss for gender prediction
Evaluation
gender_model.evaluate(test_x, test_y, verbose=1)
모델은 다음의 검증 손실과 정확도를 갖는다.
[0.07324957040103375, 0.9744245524655362]
Confusion matrix
나이 예측 대신 성별 예측은 진짜 분류 문제이다. 정확도는 모니터링해야하는 유일한 지표가 될 수 없다. 정밀도와 재현률 또한 점검되어야 한다.
predictions = gender_model.predict(test_x)
pred_list = []; actual_list = []
for i in predictions:
pred_list.append(np.argmax(i))
for i in test_y:
actual_list.append(np.argmax(i))
confusion_matrix(actual_list, pred_list)
모델은 다음과 같은 confusion matrix를 생성한다.
Prediction | Female | Male | Actual | FemaleM | 1873 | 98 | Male | 72 | 4604 |
위 표는 96.29%의 정밀도, 95.05%의 재현률을 의미한다. 이들 지표들은 정확도만큼 남족스럽다.
Testing gender for custom images
모델에 이미지를 전달하기만 하면 된다.
picture = "katy-perry.jpg"
prediction = gender_model.predict(loadImage(picture))
img = image.load_img(picture)#, target_size=(224, 224))
plt.imshow(img)
plt.show()
gender = "Male" if np.argmax(prediction) == 1 else "Female"
print("gender: ", gender)
Conclusion
우리는 ETH Zurich의 컴퓨터 비전 그룹의 연구 기사를 바탕으로 겉보기 나이와 성별 예측기를 기초에서부터 만들었다. 특히 그들이 겉보기 나이를 계산하기 위해 제안한 방법은 over-performing novel method이다. 딥러닝은 실제로 학습에서 무제한적인 힘을 갖는다.
이 글에서 사용된 겉보기 나이 예측 소스코드는 여기에서 성별 예측은 여기에서 찾을 수 있다. 유사하게 실시간 나이와 성별 예측 구현은 여기에서 찾을 수 있다. 단지 사전 훈련된 가중치만을 사용하고 싶다면 나이는 여기에 성별은 여기에서 찾을 수 있다.
Python library
DeepFace는 경량 얼굴 분석 프레임워크로 얼굴인식과 나이, 성별, 인종과 감정 같은 인구통계학을 포함한다. 기본부터 신경망 모델을 구축하는 것에 흥미가 있다면 딥페이스를 도입할 것이다. 딥페이스는 완전하게 오픈소시이고 PyPi로 사용가능하다. 여러분은 몇줄의 코드로 예측을 만들 수 있다.
Deep Face Analysis
아래 비디오에서 단지 몇줄의 코드로 파이썬에서 얼굴 특성 분석을 적용하는 방법을 볼 수 있다.
웹캠으로 실시간으로 딥페이스를 실행할 수도 있다.