更新于 

系统实现

实验环境

本地环境

对于网络的构建,我们使用本地环境先行验证。本地环境的版本参数如下表所示。

版本
Python 3.7.2
Tensorflow 2.6.2
Keras 2.6.0
OpenCV 3.4.2

使用GPU训练

我们使用Kaggle提供的在线环境训练我们的神经网络,配置有GPU模块。

然后将训练完的模型,再适配至本地环境,进行真实的人脸表情识别预测。

数据集概览

FER2013数据集共有35887个样本,如下面的输出所示。

1
2
3
data = pd.read_csv('../input/fer2013/fer2013.csv')
# 查看数据集形状
data.shape
Output
(35887, 3)

我们使用的FER2013数据集,以CSV格式呈现,如下图所示。

数据集概览
数据集概览
  • 第0列是表情对应的数字类别,从0~1分别对应着表情:Angry(生气)、Disgust(厌恶)、Fear(害怕)、Happy(高兴)、Sad(生气)、Surprise(惊讶)、Neutral(中立)。

  • 第二列是图像的像素数据,以行向量的形式呈现,使用空格分隔。像素值介于[0,255]之间。

  • 第三列是该样本的用途,有Training、PublicTest、PrivateTest。

1
2
3
#查看数据集的分类情况
#80% 训练, 10% 验证 and 10% 测试
data.Usage.value_counts()

从输出结果可知,训练数据有80%的占比,测试数据和验证数据各占10%。

Output
Training       28709
PublicTest 3589
PrivateTest 3589
Name: Usage, dtype: int64

样本概览

查看表情分类数据,如下图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#查看表情分类数据
emotion_map = {0: 'Angry', 1: 'Disgust', 2: 'Fear', 3: 'Happy', 4: 'Sad', 5: 'Surprise', 6: 'Neutral'}
emotion_counts = data['emotion'].value_counts().sort_index().reset_index()
emotion_counts.columns = ['emotion', 'number']
emotion_counts['emotion'] = emotion_counts['emotion'].map(emotion_map)
emotion_counts

# %%
# 绘制类别分布条形图
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
plt.figure(figsize=(6, 4))
sns.barplot(x=emotion_counts.emotion, y=emotion_counts.number)
plt.title('Class distribution')
plt.ylabel('Number', fontsize=12)
plt.xlabel('Emotions', fontsize=12)
plt.show()
统计表
统计表
条形图
条形图

从上面的图我们可以知道,Disgust类的样本数量比较少,这是一个分布不太均匀的数据集。

图片样本示例
图片样本示例

上图所示的是一些样本的示例图片,其生成代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def row2image_label(row):
pixels, emotion = row['pixels'], emotion_map[row['emotion']]
img = np.array(pixels.split())
img = img.reshape(48, 48)
image = np.zeros((48, 48, 3))
image[:, :, 0] = img
image[:, :, 1] = img
image[:, :, 2] = img
return image.astype(np.uint8), emotion

%matplotlib inline
%config InlineBackend.figure_format = 'svg'
plt.figure(0, figsize=(16, 10))
for i in range(1, 8):
face = data[data['emotion'] == i - 1].iloc[0]
img, label = row2image_label(face)
plt.subplot(2, 4, i)
plt.imshow(img)
plt.title(label)

plt.show()

数据预处理

数据预处理部分,主要完成了下面四个事情:

  1. 分割数据为3个部分: train, validation, test
  2. 将数据标签由字符串改为整数
  3. 调整图片大小为 48x48, 归一化图像
  4. 更改标签编码为one-hot, 例如类别3(Happy)对应为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#分割数据为: train, validation, test
data_train = data[data['Usage'] == 'Training'].copy()
data_val = data[data['Usage'] == 'PublicTest'].copy()
data_test = data[data['Usage'] == 'PrivateTest'].copy()
print(f"train shape: {data_train.shape}")
print(f"validation shape: {data_val.shape}")
print(f"test shape: {data_test.shape}")

# %%
# 绘制train, val, test的条形图
emotion_labels = ['Angry', 'Disgust', 'Fear', 'Happy', 'Sad', 'Surprise', 'Neutral']


def setup_axe(axe, df, title):
df['emotion'].value_counts().sort_index().plot(ax=axe, kind='bar', rot=0,
color=['r', 'g', 'b', 'r', 'g', 'b', 'r'])
axe.set_xticklabels(emotion_labels)
axe.set_xlabel("Emotions")
axe.set_ylabel("Number")
axe.set_title(title)

# 使用上述列表设置单个条形标签
for i in axe.patches:
# get_x pulls left or right; get_height pushes up or down
axe.text(i.get_x() - .05, i.get_height() + 120,
str(round((i.get_height()), 2)), fontsize=14, color='dimgrey',
rotation=0)

%matplotlib inline
%config InlineBackend.figure_format = 'svg'
fig, axes = plt.subplots(1, 3, figsize=(20, 8), sharey='all')
setup_axe(axes[0], data_train, 'Train')
setup_axe(axes[1], data_val, 'Validation')
setup_axe(axes[2], data_test, 'Test')
plt.show()

经过处理后的各个子数据集的样本分布情况如下图所示。

子数据集分布情况
子数据集分布情况
1
2
3
4
5
6
7
8
9
10
11
def CRNO(df, dataName):
df['pixels'] = df['pixels'].apply(lambda pixel_sequence: [int(pixel) for pixel in pixel_sequence.split()])
data_X = np.array(df['pixels'].tolist(), dtype='float32').reshape(-1, width, height, 1) / 255.0
data_Y = to_categorical(df['emotion'], num_classes)
print(dataName, f"_X shape: {data_X.shape}, ", dataName, f"_Y shape: {data_Y.shape}")
return data_X, data_Y


train_X, train_Y = CRNO(data_train, "train") #training data
val_X, val_Y = CRNO(data_val, "val") #validation data
test_X, test_Y = CRNO(data_test, "test") #test data

各子数据集的输入和预期输出的形状如下:

Output
train _X shape: (28709, 48, 48, 1),  train _Y shape: (28709, 7)
val _X shape: (3589, 48, 48, 1), val _Y shape: (3589, 7)
test _X shape: (3589, 48, 48, 1), test _Y shape: (3589, 7)

构建我们的神经网络

我们的神经网络的整体结构已在前文中给出, 每个层的具体参数如下表所示。

网络各层参数

具体代码如下所示,注意到代码中已经设置了SGD的具体优化参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# ## 构建我们的CNN
#
# ### CNN 结构:
# Conv Sages 1 --> Conv Stages 2 --> Conv Stages 3 --> Conv Stages 4 --> Flatten --> Full Connection --> Softmax Output Layer
#
# ### Conv Stages
# Conv Block --> Max Pooling
#
# ### Conv Block
# Conv --> BN --> ReLU

# %%
model = Sequential()

# ---------- Convolutional Stages 1 ----------
# ***** Conv Block a *****
model.add(Conv2D(64, kernel_size=(3, 3), input_shape=(width, height, 1),
data_format='channels_last', padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
# ***** Conv Block b *****
model.add(Conv2D(64, kernel_size=(3, 3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
# max pooling
model.add(MaxPooling2D(pool_size=(2, 2)))

# ---------- Convolutional Stages 2 ----------
# ***** Conv Block a *****
model.add(Conv2D(128, kernel_size=(3, 3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
# ***** Conv Block b *****
model.add(Conv2D(128, kernel_size=(3, 3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
# max pooling
model.add(MaxPooling2D(pool_size=(2, 2)))

# ---------- Convolutional Stages 3 ----------
# ***** Conv Block a *****
model.add(Conv2D(256, kernel_size=(3, 3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
# ***** Conv Block b *****
model.add(Conv2D(256, kernel_size=(3, 3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
# max pooling
model.add(MaxPooling2D(pool_size=(2, 2)))

# ---------- Convolutional Stages 4 ----------
# ***** Conv Block a *****
model.add(Conv2D(512, kernel_size=(3, 3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
# ***** Conv Block b *****
model.add(Conv2D(512, kernel_size=(3, 3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
# max pooling
model.add(MaxPooling2D(pool_size=(2, 2)))

# Flatten
model.add(Flatten())

# Full connection
model.add(Dense(4096, activation='relu', kernel_regularizer=l2()))
model.add(Dropout(rate_drop))
model.add(Dense(4096, activation='relu', kernel_regularizer=l2()))
model.add(Dropout(rate_drop))

#output layer
model.add(Dense(num_classes, activation='softmax', kernel_regularizer=l2()))

model.compile(loss=['categorical_crossentropy'],
optimizer=SGD(momentum=0.9, nesterov=True ,decay=1e-4),
metrics=['accuracy'])

model.summary()

数据增强

根据前文,使用Keras框架自带的ImageDataGenerator方法,编写如下代码。

1
2
3
4
5
6
7
8
# 数据增强
data_generator = ImageDataGenerator(
zoom_range=0.2,
width_shift_range=0.2,
height_shift_range=0.2,
rotation_range=10,
featurewise_std_normalization=False,
horizontal_flip=True)