The most expressive way humans display emotions is through facial expressions. Humans detect and interpret faces and facial expressions in a scene with little or no effort. An automated system could also be possible to accomplish this task by using a deep learning model that will be train based on Convolutional Neural Networks and Residual Blocks to detect facial expressions.
Problem:
Dataset:
Source:
# Import libraries
import cv2
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
import os
import PIL
from tensorflow.keras import layers
import tensorflow as tf
import pickle
from tensorflow.python.keras import *
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras import layers, optimizers
from tensorflow.keras.applications import DenseNet121
from tensorflow.keras.applications.resnet50 import ResNet50
from tensorflow.keras.layers import *
from tensorflow.keras.models import Model, load_model
from tensorflow.keras.initializers import glorot_uniform
from tensorflow.keras.utils import plot_model
from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping, ModelCheckpoint, LearningRateScheduler
from IPython.display import display
from tensorflow.keras import backend as K
# Import data
emotion_df = pd.read_csv('emotion.csv')
# Check data
# Picture is represented by number based on pixel
emotion_df.head()
# Check data
emotion_df['emotion'][2]
# Add space # String format
emotion_df['pixels'] = emotion_df[' pixels']
# Check data # String format
emotion_df['pixels'][2]
# Function to convert pixel values in string format to array format
def string2array(x):
return np.array(x.split(' ')).reshape(48, 48, 1).astype('float32')
# Apply the function
emotion_df['pixels'] = emotion_df['pixels'].apply(lambda x: string2array(x))
# Check shape
emotion_df['pixels'][2].shape
# Check data
emotion_df['pixels'][2];
# Checking for the presence of null values in the data frame
emotion_df.isnull().sum()
# Create label
label_to_text = {0:'anger', 1:'disgust', 2:'sad', 3:'happiness', 4: 'surprise'}
# Check image
plt.imshow(emotion_df['pixels'][0].squeeze(), cmap = 'gray')
plt.show()
# Create List
emotions = [0,1,2,3,4]
# Create function
for i in emotions:
data = emotion_df[emotion_df['emotion'] == i][:1]
img = data['pixels'].item()
img = img.reshape(48,48)
plt.figure()
plt.title(label_to_text[i])
plt.imshow(img, cmap= 'gray')
# Create barplot
plt.figure(figsize = (10, 10))
sns.barplot(x = emotion_df.emotion.value_counts().index, y = emotion_df['emotion'].value_counts())
plt.title("Number of images per emotion")
plt.show()
# Split the dataframe to features and labels
# From keras.utils import to_categorical
X = emotion_df['pixels']
X = np.stack(X, axis = 0)
X = X.reshape(24568, 48, 48, 1)
# y = to_categorical(emotion_df['emotion'])
y = pd.get_dummies(emotion_df['emotion'])
# Show data dimensions
print(X.shape, y.shape)
# Transform data into arrays
X = np.array(X)
y = np.array(y)
# Import lirabries
from sklearn.model_selection import train_test_split
# spliting the dataframe in to train,test and validation data frames
X_train, X_Test, y_train, y_Test = train_test_split(X,y,test_size = 0.1, shuffle = True)
X_val, X_Test, y_val, y_Test = train_test_split(X_Test,y_Test, test_size = 0.5, shuffle = True)
# Check train dataset
print(X_train.shape, y_train.shape)
# Check validation dataset
print(X_val.shape, y_val.shape)
# Check test dataset
print(X_Test.shape, y_Test.shape)
# Normalize images
X_train = X_train/255
X_val = X_val /255
X_Test = X_Test/255
# Data augmentation
train_datagen = ImageDataGenerator(
rotation_range = 15,
width_shift_range = 0.1,
height_shift_range = 0.1,
shear_range = 0.1,
zoom_range = 0.1,
fill_mode = "nearest"
)
# Create funchtion
def res_block(X, filter, stage):
# Convolutional_block
X_copy = X
f1 , f2, f3 = filter
# Main Path
X = Conv2D(f1, (1,1),strides = (1,1), name ='res_'+str(stage)+'_conv_a', kernel_initializer= glorot_uniform(seed = 0))(X)
X = MaxPool2D((2,2))(X)
X = BatchNormalization(axis =3, name = 'bn_'+str(stage)+'_conv_a')(X)
X = Activation('relu')(X)
X = Conv2D(f2, kernel_size = (3,3), strides =(1,1), padding = 'same', name ='res_'+str(stage)+'_conv_b', kernel_initializer= glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis =3, name = 'bn_'+str(stage)+'_conv_b')(X)
X = Activation('relu')(X)
X = Conv2D(f3, kernel_size = (1,1), strides =(1,1),name ='res_'+str(stage)+'_conv_c', kernel_initializer= glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis =3, name = 'bn_'+str(stage)+'_conv_c')(X)
# Short path
X_copy = Conv2D(f3, kernel_size = (1,1), strides =(1,1),name ='res_'+str(stage)+'_conv_copy', kernel_initializer= glorot_uniform(seed = 0))(X_copy)
X_copy = MaxPool2D((2,2))(X_copy)
X_copy = BatchNormalization(axis =3, name = 'bn_'+str(stage)+'_conv_copy')(X_copy)
# ADD
X = Add()([X,X_copy])
X = Activation('relu')(X)
# Identity Block 1
X_copy = X
# Main Path
X = Conv2D(f1, (1,1),strides = (1,1), name ='res_'+str(stage)+'_identity_1_a', kernel_initializer= glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis =3, name = 'bn_'+str(stage)+'_identity_1_a')(X)
X = Activation('relu')(X)
X = Conv2D(f2, kernel_size = (3,3), strides =(1,1), padding = 'same', name ='res_'+str(stage)+'_identity_1_b', kernel_initializer= glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis =3, name = 'bn_'+str(stage)+'_identity_1_b')(X)
X = Activation('relu')(X)
X = Conv2D(f3, kernel_size = (1,1), strides =(1,1),name ='res_'+str(stage)+'_identity_1_c', kernel_initializer= glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis =3, name = 'bn_'+str(stage)+'_identity_1_c')(X)
# ADD
X = Add()([X,X_copy])
X = Activation('relu')(X)
# Identity Block 2
X_copy = X
# Main Path
X = Conv2D(f1, (1,1),strides = (1,1), name ='res_'+str(stage)+'_identity_2_a', kernel_initializer= glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis =3, name = 'bn_'+str(stage)+'_identity_2_a')(X)
X = Activation('relu')(X)
X = Conv2D(f2, kernel_size = (3,3), strides =(1,1), padding = 'same', name ='res_'+str(stage)+'_identity_2_b', kernel_initializer= glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis =3, name = 'bn_'+str(stage)+'_identity_2_b')(X)
X = Activation('relu')(X)
X = Conv2D(f3, kernel_size = (1,1), strides =(1,1),name ='res_'+str(stage)+'_identity_2_c', kernel_initializer= glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis =3, name = 'bn_'+str(stage)+'_identity_2_c')(X)
# ADD
X = Add()([X,X_copy])
X = Activation('relu')(X)
return X
# Input tensor shape
input_shape = (48, 48, 1)
# Input tensor shape
X_input = Input(input_shape)
# Zero-padding
X = ZeroPadding2D((3, 3))(X_input)
# 1 - stage
X = Conv2D(64, (7, 7), strides= (2, 2), name = 'conv1', kernel_initializer= glorot_uniform(seed = 0))(X)
X = BatchNormalization(axis =3, name = 'bn_conv1')(X)
X = Activation('relu')(X)
X = MaxPooling2D((3, 3), strides= (2, 2))(X)
# 2 - stage
X = res_block(X, filter= [64, 64, 256], stage= 2)
# 3 - stage
X = res_block(X, filter= [128, 128, 512], stage= 3)
# 4 - stage
# X = res_block(X, filter= [256, 256, 1024], stage= 4)
# Average Pooling
X = AveragePooling2D((2, 2), name = 'Averagea_Pooling')(X)
# Final layer
X = Flatten()(X)
X = Dense(5, activation = 'softmax', name = 'Dense_final', kernel_initializer= glorot_uniform(seed=0))(X)
# Model architecture
model_emotion = Model( inputs= X_input, outputs = X, name = 'Resnet18')
# Check model summary
model_emotion.summary()
# Compile the network
model_emotion.compile(optimizer = "Adam", loss = "categorical_crossentropy", metrics = ["accuracy"])
# Using early stopping to exit training if validation loss is not decreasing even after certain epochs (patience)
earlystopping = EarlyStopping(monitor = 'val_loss', mode = 'min', verbose = 1, patience = 20)
# Save the best model with lower validation loss
checkpointer = ModelCheckpoint(filepath = "FacialExpression_weights.hdf5", verbose = 1, save_best_only=True)
# Train the model
history = model_emotion.fit(train_datagen.flow(X_train, y_train, batch_size = 64),
validation_data = (X_val, y_val), steps_per_epoch = len(X_train) // 64,
epochs= 10, callbacks = [checkpointer, earlystopping])
# Saving the model architecture to json file for future use
model_json = model_emotion.to_json()
with open("Emotion-model.json","w") as json_file:
json_file.write(model_json)
# Import model architecture
with open('Emotion-model.json', 'r') as json_file:
json_savedModel= json_file.read()
# load the model architecture
model_emotion = tf.keras.models.model_from_json(json_savedModel)
model_emotion.load_weights('FacialExpression_weights.hdf5')
model_emotion.compile(optimizer = "Adam", loss = "categorical_crossentropy", metrics = ["accuracy"])
# Check test accuracy
score = model_emotion.evaluate(X_Test, y_Test)
print('Test Accuracy: {}'.format(score[1]))
# Check dictionary
history.history.keys()
# Check evaluation metrics
accuracy = history.history['accuracy']
val_accuracy = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
# Number of iterations
epochs = range(len(accuracy))
# Create plot for accuracy
plt.plot(epochs, accuracy, 'bo', label='Training Accuracy')
plt.plot(epochs, val_accuracy, 'b', label='Validation Accuracy')
plt.title('Training and Validation Accuracy')
plt.legend()
plt.show()
# Create plot for validation loss
plt.plot(epochs, loss, 'ro', label='Training loss')
plt.plot(epochs, val_loss, 'r', label='Validation loss')
plt.title('Training and Validation loss')
plt.legend()
plt.show()
# Predicted_classes = model.predict_classes(X_test)
predicted_classes = np.argmax(model_emotion.predict(X_Test), axis=-1)
y_true = np.argmax(y_Test, axis=-1)
# Check data dimension
y_true.shape
# import library
from sklearn.metrics import confusion_matrix
# Confusion matrix
cm = confusion_matrix(y_true, predicted_classes)
plt.figure(figsize = (10, 10))
sns.heatmap(cm, annot = True, cbar = False)
plt.show()
# Length
L = 5
# Width
W = 5
fig, axes = plt.subplots(L, W, figsize = (24, 24))
axes = axes.ravel()
for i in np.arange(0, L*W):
axes[i].imshow(X_Test[i].reshape(48,48), cmap = 'gray')
axes[i].set_title('Prediction = {}\n True = {}'.format(label_to_text[predicted_classes[i]], label_to_text[y_true[i]]))
axes[i].axis('off')
plt.subplots_adjust(wspace = 1)
plt.show()
# Import lirabry
from sklearn.metrics import classification_report
# Check classification report
print(classification_report(y_true, predicted_classes))
It can be observed that the photos showing disgust has the lowest accuracy score it is because the data was imbalance. The solution to that is to add more examples by generating more augmented images or adjusting the weights of disgust photos. the model was able to obtained 65% accuracy with only 10 iterations. There are several related problems: detection of an image segment as a face, extraction of the facial expression information, and classification of the expression.This could be further improve by tuning the model hyper parameters and increasing the training iterations.