Detector de sorriso com Redes Neurais Convolucionais

Original article can be found here (source): Deep Learning on Medium

Detector de sorriso com Redes Neurais Convolucionais

Detectar sorrisos em faces já não uma tarefa difícil para os algoritmos atuais de machine learning. Atualmente conseguimos não só verificar se uma pessoa está sorrindo ou não, mas também saber se elá esta feliz, triste, nervosa e etc. Portando, o objetivo desse artigo não é sobre trazer uma nova metodologia ou algo melhor para detecção de sorrisos, mas apenas sobre uma forma de usar Convolucional Neural Networks (CNN’s) [1] para classificação de diferentes estados em imagens e neste caso com 98,9% de precisão.

Este problema em detectar sorrisos em imagens foi um teste proposto por um a empresa durante seleção para vaga de cientista de dados. Para resolver o problema tive que escolher um método, como estava sem tempo e o teste tinha prazo resolvi, usar a CNN. O principal motivo para essa escolha foi que para realizar o treinamento das CNN’s as etapas de pré-processamento são menores, uma vez que a própria arquitetura realiza grande parte dessa etapa. Na imagem abaixo podemos ver uma estrutura similar da rede neural que iremos construir.

Banco de Dados

O banco de dados usado foi o LFWcrop Face Dataset. Para detectar sorrisos em faces, a imagem estar colorida não é um atributo relevante na minha opinião, então usei o dataset com somente imagens em escala cinza. Ele possui 13233 imagens de rostos com dimensões 64×64, porém não sabemos quais imagens possui pessoas sorrindo ou não então para isso usamos duas listas NON-SMILE_list.txt e SMILE_list.txt [2] em cada arquivo de texto contém os nomes das imagens que devem ser usadas o treinamento.

Pré-processamento

Já que o tio Ned disse…vamos ao código, porém antes de construirmos a rede neural precisamos preparar nosso banco de dados de forma adequada para que possamos realizar o treinamento.

Primeiro faço a importação de todas as bibliotecas que iremos usar.

import os 
from glob import glob
from keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img
import pandas as pd
from shutil import copyfile,rmtree
import matplotlib.pyplot as plt
import numpy as np

from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout
from keras.layers.normalization import BatchNormalization
from keras.callbacks import ModelCheckpoint, EarlyStopping, TensorBoard

Agora realizamos leitura dos documentos ‘.txt’ [2] citados anteriormente que indica quais imagens devem ser usadas para o treinamento. Assim foi gerado 2 listas, uma com nomes das imagens com pessoas sorrindo e outra que não, cada lista tinha cerca de 599 e 602 nomes de imagens respectivamente. Em seguida foi dividido cada lista em dados de treinamento (70%), teste (20%) e validação(10%). Obtendo assim 6 listas:

  • train_smile
  • test_smile
  • val_smile
  • train_nosmile
  • test_nosmile
  • val_nosmile

Leitura dos arquivos .txt e armazenando o conteúdo em listas

smile_txt = open("./SMILE_list.txt", "r")
nosmile_txt = open("./NON-SMILE_list.txt", "r")

all_images = glob('./lfwcrop_grey/faces/*.pgm')

lista_smile = smile_txt.readlines()
lista_nosmile = nosmile_txt.readlines()

smile = []
nosmile = []
path = "./lfwcrop_grey/faces/"
for line in lista_smile:# remover a quebra de linha '/n' e extensão
c = line[:-5]

c = c+".pgm"

if path + c in all_images:


smile.append(c)

for line in lista_nosmile:# remover a quebra de linha '/n' e extensão
c = line[:-5]

c = c+".pgm"

if path + c in all_images:


nosmile.append(c)


ns = len(smile)
nns = len(nosmile)
print("Smile: %s, No-Smile: %s" %(ns,nns ))

Divisão dos dados de treinamento, teste e validação

############### Smile train, teste, val 
n_train = int (ns - (ns * 0.3))
n_test = int (ns * 0.20)
n_val = int (ns * 0.10)
total = n_test + n_train + n_valprint("SMILE -->N Train: %s N test: %s, N val: %s" %(n_train, n_test, n_val))
print(total)
train_smile = smile[:n_train] #70%
test_smile = smile[n_train:n_train+n_test] #20%
val_smile = smile[n_train+n_test:] #10%
print("SMILE -->Train %s, Teste %s, Val %s" %(len(train_smile),len(test_smile), len(val_smile) ))
############### Nosmile train, teste, val
n_train = int (nns - (nns * 0.3))
n_test = int (nns * 0.20)
n_val = int (nns * 0.10)
total = n_test + n_train + n_valprint("NOSMILE -->N Train: %s N test: %s, N val: %s" %(n_train, n_test, n_val))
print(total)
train_nosmile = nosmile[:n_train] #70%
test_nosmile = nosmile[n_train:n_train+n_test] #20%
val_nosmile = nosmile[n_train+n_test:] #10%
print("NOSMILE -->Train %s, Teste %s, Val %s" %(len(train_nosmile),len(test_nosmile), len(val_nosmile) ))

O próximo passo foi criar as pastas que receberá as imagens de cada lista, então é criado os diretórios Train, Test e Val e dentro de cada uma foi criado mais duas pastas smile e nosmile. Em seguida, é feito uma cópia das imagens do dataset original e é movido essas cópias para as pastas de acordo com a lista que ela pertence.

A criação das pastas e as cópias das imagens poderia ser feita manualmente, porém aqui irei realizar tudo via código.

Podemos ver um esquema na figura abaixo

É realizado esse procedimento para que os dados estejam de acordo com o que a função de pré-processamento ImageDataGenerator.flow_from_directory [3] do pacote Keras solicita, a função consegue ler dentro de cada pasta e rotular as classes (Smile e Nosmile) automaticamente. Foi usado esse método pois ele auxilia no pré-processamento das imagens. Além disso, é importante lembrar que reduzimos bastante nosso banco de dados de 13233 imagens para cerca de 1201 ( 599 smile e 602 no-smile), então devido o nosso conjunto de dados não ser muito grande, realizamos também com essa função o aumento artificialmente do conjunto de dados.

Criando pastas

try: 
rmtree("./lfwcrop_grey/data")
rmtree("./lfwcrop_grey/data2")

except:
pass
finally:
os.mkdir("./lfwcrop_grey/data")
os.mkdir("./lfwcrop_grey/data2")
os.mkdir("./lfwcrop_grey/data/train")
os.mkdir("./lfwcrop_grey/data/train/smile")
os.mkdir("./lfwcrop_grey/data/train/nosmile")
os.mkdir("./lfwcrop_grey/data/test")
os.mkdir("./lfwcrop_grey/data/test/smile")
os.mkdir("./lfwcrop_grey/data/test/nosmile")
os.mkdir("./lfwcrop_grey/data/val")
os.mkdir("./lfwcrop_grey/data/val/smile")
os.mkdir("./lfwcrop_grey/data/val/nosmile")

Copiando as imagens para as pastas

for i in train_smile: 
#print(i)
copyfile("./lfwcrop_grey/faces/"+i, "./lfwcrop_grey/data/train/smile/"+i[:-4]+".jpg")

for i in train_nosmile:
#print(i)
copyfile("./lfwcrop_grey/faces/"+i, "./lfwcrop_grey/data/train/nosmile/"+i[:-4]+".jpg")


for i in test_smile:
#print(i)
copyfile("./lfwcrop_grey/faces/"+i, "./lfwcrop_grey/data/test/smile/"+i[:-4]+".jpg")

for i in test_nosmile:
#print(i)
copyfile("./lfwcrop_grey/faces/"+i, "./lfwcrop_grey/data/test/nosmile/"+i[:-4]+".jpg")

for i in val_smile:
#print(i)
copyfile("./lfwcrop_grey/faces/"+i, "./lfwcrop_grey/data/val/smile/"+i[:-4]+".jpg")

for i in val_nosmile:
#print(i)
copyfile("./lfwcrop_grey/faces/"+i, "./lfwcrop_grey/data/val/nosmile/"+i[:-4]+".jpg")

Aqui iremos realmente usar a função ImageDataGenerator, primeiro definimos quais tipo de modificações iremos realizar nas imagens em cada interação. Por exemplo, na geração dos dados de treinamento (datagen_train) iremos realizar operações como zoom_range, em que as imagens irão sofrer diferentes ajustes de zoom em uma escala determinada. Como queremos testar nosso modelo com imagens “normais” na geração dos dados de teste e validação as imagens não sofreram modificações, exceto o rescale apenas para normalizar as imagens.

from keras.preprocessing.image import ImageDataGeneratordatagen_train = ImageDataGenerator(
rescale = 1./255,
width_shift_range = 0.1, # Altera de forma randômica as imagens horizontalmente (10% da largura total)
height_shift_range = 0.1, # Altera de forma randômica as imagens verticalmente (10% da altura total)
zoom_range = 0.1,
#horizontal_flip = True # De forma randômica inverte imagens horizontalmente
)
datagen_test = ImageDataGenerator(
rescale = 1./255
)


datagen_val = ImageDataGenerator(
rescale = 1./255
)

No código a seguir, importamos as imagens do caminho do diretório informado no primeiro parâmetro da função flow_from_directory. Outro importante parâmetro é o batch_size pois nele é onde informamos quantas cópias de imagens modificadas iremos gerar em cada época de treinamento.

Fazendo um resumo, em cada época de treinamento irão ser geradas imagens artificiais a partir de uma original e a quantidade de imagens é determinado pelo batch_size. Assim, nosso modelo receberá uma maior quantidade e diferentes imagens a cada época, o que ajuda bastante para o nosso modelo ter uma acurácia melhor, nesse caso ajudou o modelo a conseguir cerca de 7% precisão a mais.

data_train = datagen_train.flow_from_directory(
'./lfwcrop_grey/data/train/',
target_size=(64,64),
batch_size=2,
class_mode="binary",
color_mode="grayscale",
save_to_dir="./lfwcrop_grey/data2",
save_format='png',
save_prefix='aug'

)
data_test = datagen_train.flow_from_directory(
'./lfwcrop_grey/data/test/',
target_size=(64,64),
batch_size=2,
class_mode="binary",
color_mode="grayscale"
)
data_val = datagen_train.flow_from_directory(
'./lfwcrop_grey/data/val/',
target_size=(64,64),
batch_size=1,
class_mode="binary",
color_mode="grayscale"
)

Modelo CNN

O modelo construído tem duas camadas de convoluções e duas camadas densas com 128 neurônios cada. Para conseguir evitar um super aprendizado (overffting) , em que a rede acaba “memorizando” as imagens ao invés de aprender os padrões delas e tem dificuldade de classificar novos conteúdos que no caso seria as imagens de validação e teste, e para isso foi usado camadas de Dropout [4].

model = Sequential()model.add(Conv2D(32, (3,3), input_shape = (64,64,1), activation='relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(32, (3,3), activation='relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2))
model.add(Flatten())model.add(Dense(128, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(1, activation = 'sigmoid'))

Treinando o modelo

O modelo propõe um treinamento de 10 épocas, porém devido um parâmetro que foi adicionado EarlyStopping, quando o modelo começar a diminuir sua precisão de acerto nos dados de validação, o treinamento irá ser interrompido.

model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])check = ModelCheckpoint(filepath='weights.hdf5', verbose=1, save_best_only=True)
early = EarlyStopping(monitor='val_loss',patience=2)
#tensorboard = TensorBoard(log_dir='./logs')
model.fit_generator(data_train, steps_per_epoch=840, epochs=10, validation_steps=122, validation_data=data_val,
use_multiprocessing=True,
callbacks=[check, early, tensorboard]

)

Após treinamento conseguir uma acurácia de 98.91% nos dados treinamento, 96.46% nos dados de validação.

Verificando precisão em dados de teste

score = model.evaluate_generator(data_test,steps= 239, verbose=1)
print(score)

Nos dados de teste obtive uma acurácia de 96% na classificação.

x,y = data_test.next()pred = model.predict(x)if pred[0] == 1:
result = "SMILE"
#print (result)
else:
result = "NO-SMILE"
#print (result)
image = x[0]
plt.imshow(image.reshape(64, 64), cmap=plt.get_cmap('gray'))
plt.text(0,60, result, fontsize=24,color='red')
plt.show()

Código completo e os todos requirimentos para executar está no meu GitHub, lá também é possível encontrar outros projetos que realizei.

LinkedIn

Portfólio

GitHub

Referências e Links úteis