【CV】使用Keras和迁移学习从人脸图像中预测体重指数BMI
作者:Leo Simmons? ?編譯:ronghuaiyang
導(dǎo)讀
和人臉屬性預(yù)測非常相似的一個應(yīng)用。
這篇文章描述了一個神經(jīng)網(wǎng)絡(luò),它可以通過人臉圖像預(yù)測一個人的BMI([身體質(zhì)量指數(shù)])。這個項目借鑒了另一個項目:https://github.com/yu4u/age-gender-estimation的方法,通過人臉來對一個人的年齡和性別進(jìn)行分類,這個項目包括一個訓(xùn)練過的模型的權(quán)重和一個腳本,該腳本用攝像頭動態(tài)檢測用戶的臉。這除了是一個有趣的機(jī)器學(xué)習(xí)問題外,以這種方式預(yù)測BMI可能是一個有用的醫(yī)學(xué)診斷工具。
訓(xùn)練數(shù)據(jù)
使用的訓(xùn)練數(shù)據(jù)是4000張圖像,每張都是不同個體的圖像,都是從受試者的正面拍攝的。每個訓(xùn)練樣本的BMI由受試者的身高和體重計算(BMI是體重(kg)除以身高(米)的平方)。雖然訓(xùn)練圖像不能在這里分享,因為它們被用于另一個私人項目,但這種類型的數(shù)據(jù)可以從網(wǎng)上的不同地方收集。
圖形預(yù)處理
為了在訓(xùn)練前對圖像進(jìn)行歸一化,將每張圖像裁剪到受試者的面部,不包括面部周圍的區(qū)域。使用Python庫dlib檢測每幅圖像中的受試者的面部,并在dlib檢測到的邊界周圍添加額外的邊界,以生成用于實際訓(xùn)練圖像。我們實驗了幾個邊距,看看哪個能讓網(wǎng)絡(luò)表現(xiàn)得最好。我們選擇了20%的邊距,即圖像的高度和寬度擴(kuò)大40%(每邊都是20%),因為它能產(chǎn)生最佳的驗證性能。
下面顯示了使用不同裁剪邊緣添加到 Bill Murray 的圖像中,還有一個表格,顯示了添加了不同的邊距在驗證集上模型可以達(dá)到的最小的平均絕對誤差(MAE)。
原始圖像使用不同的Margin進(jìn)行裁剪的圖像
使用不同的Margin的圖像進(jìn)行訓(xùn)練的最低MAE
雖然在20%-50%的margin范圍內(nèi)的MAE值可能太過接近,不能說任何一個都比其他的好,但很明顯,至少增加20%的margin 會比不增加margin 產(chǎn)生更好的MAE。這可能是因為增加的margin 捕獲了前額上部、耳朵和頸部等特征,這些特征對模型預(yù)測BMI很有用,但大部分被原始的dlib裁剪掉了。
圖像預(yù)處理代碼:
import os import cv2 import dlib from matplotlib import pyplot as plt import numpy as np import configdetector = dlib.get_frontal_face_detector()def crop_faces():bad_crop_count = 0if not os.path.exists(config.CROPPED_IMGS_DIR):os.makedirs(config.CROPPED_IMGS_DIR)print 'Cropping faces and saving to %s' % config.CROPPED_IMGS_DIRgood_cropped_images = []good_cropped_img_file_names = []detected_cropped_images = []original_images_detected = []for file_name in sorted(os.listdir(config.ORIGINAL_IMGS_DIR)):np_img = cv2.imread(os.path.join(config.ORIGINAL_IMGS_DIR,file_name))detected = detector(np_img, 1)img_h, img_w, _ = np.shape(np_img)original_images_detected.append(np_img)if len(detected) != 1:bad_crop_count += 1continued = detected[0]x1, y1, x2, y2, w, h = d.left(), d.top(), d.right() + 1, d.bottom() + 1, d.width(), d.height()xw1 = int(x1 - config.MARGIN * w)yw1 = int(y1 - config.MARGIN * h)xw2 = int(x2 + config.MARGIN * w)yw2 = int(y2 + config.MARGIN * h)cropped_img = crop_image_to_dimensions(np_img, xw1, yw1, xw2, yw2)norm_file_path = '%s/%s' % (config.CROPPED_IMGS_DIR, file_name)cv2.imwrite(norm_file_path, cropped_img)good_cropped_img_file_names.append(file_name)# save info of good cropped imageswith open(config.ORIGINAL_IMGS_INFO_FILE, 'r') as f:column_headers = f.read().splitlines()[0]all_imgs_info = f.read().splitlines()[1:]cropped_imgs_info = [l for l in all_imgs_info if l.split(',')[-1] in good_cropped_img_file_names]with open(config.CROPPED_IMGS_INFO_FILE, 'w') as f:f.write('%s\n' % column_headers)for l in cropped_imgs_info:f.write('%s\n' % l)print 'Cropped %d images and saved in %s - info in %s' % (len(original_images_detected), config.CROPPED_IMGS_DIR, config.CROPPED_IMGS_INFO_FILE)print 'Error detecting face in %d images - info in Data/unnormalized.txt' % bad_crop_countreturn good_cropped_images# image cropping function taken from: # https://stackoverflow.com/questions/15589517/how-to-crop-an-image-in-opencv-using-python def crop_image_to_dimensions(img, x1, y1, x2, y2):if x1 < 0 or y1 < 0 or x2 > img.shape[1] or y2 > img.shape[0]:img, x1, x2, y1, y2 = pad_img_to_fit_bbox(img, x1, x2, y1, y2)return img[y1:y2, x1:x2, :]def pad_img_to_fit_bbox(img, x1, x2, y1, y2):img = cv2.copyMakeBorder(img, - min(0, y1), max(y2 - img.shape[0], 0),-min(0, x1), max(x2 - img.shape[1], 0), cv2.BORDER_REPLICATE)y2 += -min(0, y1)y1 += -min(0, y1)x2 += -min(0, x1)x1 += -min(0, x1)return img, x1, x2, y1, y2if __name__ == '__main__':crop_faces()圖像增強(qiáng)
為了增加每個原始訓(xùn)練圖像用于網(wǎng)絡(luò)訓(xùn)練的次數(shù),在每個訓(xùn)練epoch中對圖像進(jìn)行增強(qiáng)。圖像增強(qiáng)庫Augmentor用于動態(tài)旋轉(zhuǎn)、翻轉(zhuǎn)和扭曲圖像不同部分的分辨率,并改變圖像的對比度和亮度。
沒有增強(qiáng)隨機(jī)增強(qiáng)
圖像增強(qiáng)代碼:
from keras.preprocessing.image import ImageDataGenerator import pandas as pd import Augmentor from PIL import Image import random import numpy as np import matplotlib.pyplot as plt import math import configdef plot_imgs_from_generator(generator, number_imgs_to_show=9):print ('Plotting images...')n_rows_cols = int(math.ceil(math.sqrt(number_imgs_to_show)))plot_index = 1x_batch, _ = next(generator)while plot_index <= number_imgs_to_show:plt.subplot(n_rows_cols, n_rows_cols, plot_index)plt.imshow(x_batch[plot_index-1])plot_index += 1plt.show()def augment_image(np_img):p = Augmentor.Pipeline()p.rotate(probability=1, max_left_rotation=5, max_right_rotation=5)p.flip_left_right(probability=0.5)p.random_distortion(probability=0.25, grid_width=2, grid_height=2, magnitude=8)p.random_color(probability=1, min_factor=0.8, max_factor=1.2)p.random_contrast(probability=.5, min_factor=0.8, max_factor=1.2)p.random_brightness(probability=1, min_factor=0.5, max_factor=1.5)image = [Image.fromarray(np_img.astype('uint8'))]for operation in p.operations:r = round(random.uniform(0, 1), 1)if r <= operation.probability:image = operation.perform_operation(image)image = [np.array(i).astype('float64') for i in image]return image[0]image_processor = ImageDataGenerator(rescale=1./255,preprocessing_function=augment_image)# subtract validation size from training data with open(config.CROPPED_IMGS_INFO_FILE) as f:for i, _ in enumerate(f):passtraining_n = i - config.VALIDATION_SIZEtrain_df=pd.read_csv(config.CROPPED_IMGS_INFO_FILE, nrows=training_n)train_generator=image_processor.flow_from_dataframe(dataframe=train_df,directory=config.CROPPED_IMGS_DIR,x_col='name',y_col='bmi',class_mode='other',color_mode='rgb',target_size=(config.RESNET50_DEFAULT_IMG_WIDTH,config.RESNET50_DEFAULT_IMG_WIDTH),batch_size=config.TRAIN_BATCH_SIZE)模型結(jié)構(gòu)
模型是使用Keras ResNet50類創(chuàng)建的。選擇ResNet50架構(gòu),權(quán)重是由一個年齡分類器訓(xùn)練得到的,來自年齡和性別的項目可用于遷移學(xué)習(xí),也因為ResNet(殘差網(wǎng)絡(luò))架構(gòu)對于人臉圖像識別是很好的模型。
其他網(wǎng)絡(luò)架構(gòu)在基于人臉的圖像分類任務(wù)上也取得了令人印象深刻的結(jié)果,未來的工作可以探索其中的一些結(jié)構(gòu)用于BMI 指數(shù)的預(yù)測。
實現(xiàn)模型架構(gòu)代碼:
from tensorflow.python.keras.models import Model from tensorflow.python.keras.applications import ResNet50 from tensorflow.python.keras.layers import Dense import configdef get_age_model():# adapted from https://github.com/yu4u/age-gender-estimation/blob/master/age_estimation/model.pyage_model = ResNet50(include_top=False,weights='imagenet',input_shape=(config.RESNET50_DEFAULT_IMG_WIDTH, config.RESNET50_DEFAULT_IMG_WIDTH, 3),pooling='avg')prediction = Dense(units=101,kernel_initializer='he_normal',use_bias=False,activation='softmax',name='pred_age')(age_model.output)age_model = Model(inputs=age_model.input, outputs=prediction)age_model.load_weights(config.AGE_TRAINED_WEIGHTS_FILE)print 'Loaded weights from age classifier'return age_modeldef get_model():base_model = get_age_model()last_hidden_layer = base_model.get_layer(index=-2)base_model = Model(inputs=base_model.input,outputs=last_hidden_layer.output)prediction = Dense(1, kernel_initializer='normal')(base_model.output)model = Model(inputs=base_model.input, outputs=prediction)return model遷移學(xué)習(xí)
遷移學(xué)習(xí)是為了利用年齡分類器網(wǎng)絡(luò)中的權(quán)重,因為這些對于檢測用于預(yù)測BMI的低級面部特征應(yīng)該是有價值的。為年齡網(wǎng)絡(luò)加一個新的線性回歸輸出層(輸出一個代表BMI的數(shù)字),并使用MAE作為損失函數(shù)和Adam作為訓(xùn)練優(yōu)化器進(jìn)行訓(xùn)練。
首先對模型進(jìn)行訓(xùn)練,使原始年齡分類器的每一層都被凍結(jié),以允許新輸出層的隨機(jī)權(quán)值進(jìn)行更新。第一次訓(xùn)練包含了10個epoch,因為在此之后,MAE沒有明顯的下降(使用early stop)。
在這個初始訓(xùn)練階段之后,模型被訓(xùn)練了30個epoch,網(wǎng)絡(luò)中的每一層都被解凍,以微調(diào)網(wǎng)絡(luò)中的所有權(quán)重。Early stopping也決定了這里的epoch的數(shù)量,只有在觀察到MAE沒有減少的10個epoch后才停止訓(xùn)練(patience為10)。由于模型在epoch 20達(dá)到了最低的驗證性MAE,訓(xùn)練在epoch 30停止。取模型在epoch 20的權(quán)重,并在下面的演示中使用。
平均絕對誤差被選作為損失函數(shù),和均方誤差(MSE)或均方根誤差(RMSE)不一樣,BMI預(yù)測的誤差的尺度是線性的(誤差為10的懲罰應(yīng)該是誤差為5的懲罰的2倍)。
模型訓(xùn)練代碼:
import cv2 import numpy as np from tensorflow.python.keras.callbacks import EarlyStopping, ModelCheckpoint, TensorBoard from train_generator import train_generator, plot_imgs_from_generator from mae_callback import MAECallback import configbatches_per_epoch=train_generator.n //train_generator.batch_sizedef train_top_layer(model):print 'Training top layer...'for l in model.layers[:-1]:l.trainable = Falsemodel.compile(loss='mean_absolute_error',optimizer='adam')mae_callback = MAECallback()early_stopping_callback = EarlyStopping(monitor='val_mae',mode='min',verbose=1,patience=1)model_checkpoint_callback = ModelCheckpoint('saved_models/top_layer_trained_weights.{epoch:02d}-{val_mae:.2f}.h5',monitor='val_mae',mode='min',verbose=1,save_best_only=True)tensorboard_callback = TensorBoard(log_dir=config.TOP_LAYER_LOG_DIR,batch_size=train_generator.batch_size)model.fit_generator(generator=train_generator,steps_per_epoch=batches_per_epoch,epochs=20,callbacks=[mae_callback,early_stopping_callback,model_checkpoint_callback,tensorboard_callback])def train_all_layers(model):print 'Training all layers...'for l in model.layers:l.trainable = Truemae_callback = MAECallback()early_stopping_callback = EarlyStopping(monitor='val_mae',mode='min',verbose=1,patience=10)model_checkpoint_callback = ModelCheckpoint('saved_models/all_layers_trained_weights.{epoch:02d}-{val_mae:.2f}.h5',monitor='val_mae',mode='min',verbose=1,save_best_only=True)tensorboard_callback = TensorBoard(log_dir=config.ALL_LAYERS_LOG_DIR,batch_size=train_generator.batch_size)model.compile(loss='mean_absolute_error',optimizer='adam')model.fit_generator(generator=train_generator,steps_per_epoch=batches_per_epoch,epochs=100,callbacks=[mae_callback,early_stopping_callback,model_checkpoint_callback,tensorboard_callback])Demo
下面是模型通過Christian Bale的幾張照片預(yù)測出的體重指數(shù)。之所以選擇貝爾作為研究對象,是因為眾所周知,他會在不同的角色中劇烈地改變自己的體重。知道了他的身高是6英尺0英寸,他的體重就可以從模型的BMI預(yù)測中得到。
左邊的圖片來自機(jī)械師,其中貝爾說他“大概135磅”。如果他的體重是135磅,那么他的BMI是18.3 kg/m (BMI的單位),而模型的預(yù)測相差約4 kg/m。中間的圖片是我認(rèn)為代表他的體重,當(dāng)時他沒有為一個角色徹底改變它。右邊的圖片是在拍攝Vice時拍攝的。在拍攝Vice的時候,我找不到他的體重數(shù)字,但我找到幾個消息來源說他胖了45磅。如果我們假設(shè)他的平均體重是200磅,而在拍攝Vice時他體重是245磅,體重指數(shù)為33.2,那么模型對這張照片的體重指數(shù)預(yù)測將相差約1 kg/m2。
下面是我的BMI預(yù)測模型的記錄。我的身體質(zhì)量指數(shù)是23 kg/m2,當(dāng)我直視相機(jī)時,模型偏差2~4 kg/m2,當(dāng)我的頭偏向一邊或者朝下時,偏差高達(dá)8kg/m2。
討論
該模型的驗證MAE為4.48。給定一個人,5“9和195磅,美國男性的平均身高和體重,BMI 為27.35kg/m2,這4.48的錯誤將導(dǎo)致預(yù)測范圍為22.87 kg/m2 到 31.83 kg/m2,對應(yīng)163和227磅重量。顯然,還有改進(jìn)的余地,今后的工作將努力減少這種錯誤。
該模型的一個明顯缺點是,當(dāng)評估從不同角度而不是從被攝者的正面拍攝的圖像時,性能很差。當(dāng)我把頭移到一邊或往下時,模型的預(yù)測就變得不那么準(zhǔn)確了。
這個模型的另一個可能的缺點可能有助于解釋這個模型對 Christian Bale的第一張照片的不準(zhǔn)確的預(yù)測,那就是當(dāng)主體在黑暗的環(huán)境中被一個集中的光源照射時,表現(xiàn)不佳。強(qiáng)烈的光照造成的陰影改變了臉的兩側(cè)的曲率和皮膚的微妙的表現(xiàn),造成了對BMI的影響。
也有可能這個模型只是簡單地高估了總體BMI較低的受試者的BMI,這可以從它對我自己和克里斯蒂安·貝爾的第一張照片的評估中看出。
該模型的這些缺點可能可以用訓(xùn)練數(shù)據(jù)中奇怪的角度、集中的光線和較低的BMIs來解釋。大多數(shù)訓(xùn)練圖像是在良好的光照下,從受試者的前部拍攝的,并且是由BMI高于25 kg/m2的受試者拍攝的。因此,在這些不同的場景中,該模型可能無法充分了解面部特征與BMI的相關(guān)性。
—END—
英文原文:https://medium.com/@leosimmons/estimating-body-mass-index-from-face-images-using-keras-and-transfer-learning-de25e1bc0212
往期精彩回顧適合初學(xué)者入門人工智能的路線及資料下載機(jī)器學(xué)習(xí)及深度學(xué)習(xí)筆記等資料打印機(jī)器學(xué)習(xí)在線手冊深度學(xué)習(xí)筆記專輯《統(tǒng)計學(xué)習(xí)方法》的代碼復(fù)現(xiàn)專輯 AI基礎(chǔ)下載機(jī)器學(xué)習(xí)的數(shù)學(xué)基礎(chǔ)專輯獲取一折本站知識星球優(yōu)惠券,復(fù)制鏈接直接打開:https://t.zsxq.com/662nyZF本站qq群1003271085。加入微信群請掃碼進(jìn)群:總結(jié)
以上是生活随笔為你收集整理的【CV】使用Keras和迁移学习从人脸图像中预测体重指数BMI的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【机器学习应用】还没期末考试,算法却说我
- 下一篇: 【科研经验】学霸为什么不喜欢给学渣解题?