1 概述

任务目标:人脸表情识别
数据来源:kaggle
模型设计:Lenet7和CliqueNet
度量标准:准确率
实验分类:8类(愤怒 恶心 害怕 快乐 悲伤 惊讶 蔑视 面无表情)

2 数据集

2.1 数据集下载

由Microsoft对kaggle提出的数据集进行重新标注的数据集
数据集下载

2.2 数据规模

训练集:测试集:验证集 = 8:1:1
1P = 48 * 48 = 2304像素
截屏2020-12-23 下午4.09.29.png

2.3 数据格式

-csv -3个属性:Label Pixels Usage
实际内容:人脸(灰度)
8中表情:愤怒 恶心 害怕 快乐 悲伤 惊讶 蔑视 面无表情(No Expression)
面部基本居中,大小相似
预处理数据下载
截屏2020-12-23 下午4.19.14.png
截屏2020-12-23 下午4.11.11.png

3 模型说明

3.1 Lenet7

截屏2020-12-23 下午4.20.45.png
Lenet7原来的网络使用两层卷积层和两层池化层,在实践中增大了模型深度,更好的适应当前数据特征
增加一个卷积和池化层
截屏2020-12-23 下午4.22.40.png
增加一层全连接层。
截屏2020-12-23 下午4.23.42.png

3.2 CliqueNet

(1)简介
由DenseNet启发
各Layer间双向连接
stage1: 浅层—>高层特征
stage2: 近层更新远层
截屏2020-12-23 下午4.25.47.png
(2)网络结构
本项目中由于数据集复杂程度不高,使用l三个block,每个block提取特征进行预测。浅层提取到了细节特征,深层提取到了全局信息。
截屏2020-12-23 下午4.26.27.png
此外在网络中使用Transition模块,使用channel-wise attention来给不同的channel赋予不同的权重。

4 代码分析

项目源码下载

4.1 cliquenet.py中forward函数

其中block模块主要对一维和高维图像特征进行提取
transition模块主要通过channel-wise attention机制对cannel的结构进行优化,以使得channel都获得所有维度的特征信息,有益于后面过程的学习。

  1. # 定义了cliquenet的前向传播函数
  2. def forward(x, train=True, regularizer=None):
  3. # 得到之后进行卷积的卷积核张量,输入channel为1,输出channel为64,kernel size为3
  4. w = get_weight([3, 3, 1, 64], 0.1, regularizer)
  5. # 先进行一次卷积,步长为2,使得feature map维度减半,使得模型提取到浅层特征
  6. x = conv2d(x, w, 2)
  7. # 对数据进行批归一化,加快网络训练,减轻梯度消失
  8. x = bn(x, train)
  9. # 使用ReLU激活函数使得模型非线性
  10. x = tf.nn.relu(x)
  11. # 对数据进行最大池化,使得feature map的维度减半
  12. x = tf.nn.max_pool(x, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
  13. ## block1
  14. # 将数据送入clique block,block内每一个结点的channel数设为36,一个block内除输入结点外有5个结点
  15. x, feature1 = clique_block(x, regularizer, 64, 36, 3, 5, train)
  16. # 对第一个block提取到的特征信息进行global pooling
  17. feature1 = tf.nn.avg_pool(feature1, ksize=[1, 12, 12, 1], strides=[1, 12, 12, 1], padding='SAME')
  18. # 将上一层的五个更新后结点作为输入,经过transition调整feature map的维度,并利用attention强化特征信息更优的channel,使得下一个block可以更好地利用前层特征信息
  19. x = transition(x, regularizer, 180, 180, 12, train)
  20. ## block 2
  21. # 将数据送入clique block,block内每一个结点的channel数设为36,一个block内除输入结点外有5个结点
  22. x, feature2 = clique_block(x, regularizer, 180, 36, 3, 5, train)
  23. # 对第二个block提取到的特征信息进行global pooling
  24. feature2 = tf.nn.avg_pool(feature2, ksize=[1, 6, 6, 1], strides=[1, 6, 6, 1], padding='SAME')
  25. # 将上一层的五个更新后结点作为输入,经过transition调整feature map的维度,并利用attention强化特征信息更优的channel,使得下一个block可以更好地利用前层特征信息
  26. x = transition(x, regularizer, 180, 180, 6, train)
  27. ## block3
  28. # 将数据送入clique block,block内每一个结点的channel数设为36,一个block内除输入结点外有5个结点
  29. _, feature3 = clique_block(x, regularizer, 180, 36, 3, 5, train)
  30. # 对第三个block提取到的特征信息进行global pooling
  31. feature3 = tf.nn.avg_pool(feature3, ksize=[1, 3, 3, 1], strides=[1, 3, 3, 1], padding='SAME')
  32. # 将不同层间获取的不同特征进行融合,浅层的特征包含更多的细节信息,而深层特征则包含更多的全局信息
  33. out = tf.concat([feature1, feature2, feature3], axis=3)
  34. # 将张量转换为一个长向量,以用于之后的全连接层
  35. x = tf.reshape(out, [-1, 964])
  36. # 将向量输入全连接层,从而输出对每一个分类的预测分数
  37. x = fc(x, 964, 7, 0.01, regularizer)
  38. # 返回该神经网络的预测结果
  39. return x

4.2 cliquenet.py中stage1和stage2函数

stage1的公式
截屏2020-12-23 下午4.40.51.png
代码结构是
layer1 = encoding(input)
layer2 =encoding(layer1,input)
layer3 = encoding(layer2,input)

layer5 = encoding(layer4,input)
因此layer5中存在提取的高维提取特征,并且有很多重复的获取的低维特征。以使得低维和高维的特征共同被利用,加强了information flow,能有效避免梯度消失。

  1. # 定义了stage1阶段
  2. def stage1(x0, w0, w, in_channel, filters, layers=5, train=True):
  3. # 使用输入结点对每一个之外的结点进行初始化
  4. for i in range(layers):
  5. # 如果是第一个结点
  6. if i == 0:
  7. # 就取w0中的第一个张量作为连接输入结点与第一个结点的卷积核
  8. weight = w0[i]
  9. # 第一个结点在stage1只与输入结点相连接
  10. data = x0
  11. # 如果不是第一个结点
  12. else:
  13. # 就取w0中对应的结点以及w中对应的结点拼成之后使用的卷积核
  14. weight = tf.concat([w0[i]] + [w[4 * num + i - 1] for num in range(i)], axis=2)
  15. # 使用拼出的卷积核与前面的结点生成一个新的结点
  16. x = conv2d(data, weight)
  17. # 对数据进行批归一化,加快网络训练,减轻梯度消失
  18. x = bn(x, train)
  19. # 使用ReLU激活函数使得模型非线性
  20. x = tf.nn.relu(x)
  21. # 使用dropout随机使部分结点归零,从而使得模型不易过拟合
  22. x = dropout(x, train)
  23. # 将已经生成的结点concat在一起用于生成下一个结点
  24. data = tf.concat([data, x], axis=3)
  25. # 除去输入结点与第一个结点(第一个结点在stage2的第一步即需要被更新,因此不需要传入下一阶段)
  26. _, x = tf.split(data, [in_channel + filters, filters * (layers - 1)], axis=3)
  27. # 将得到的后面若干个结点传入stage2
  28. return x

stage2:迭代更新layers
layer1 = encoding(layer2~5)
layer2 =encoding(layer1,3,4,5)
layer3 = encoding(layer1,2,4,5)

layer5 = encoding(layer1,2,3,4)

  1. # 定义了stage2模块
  2. def stage2(x, w, in_channel, filters, layers=5, train=True):
  3. # 对block内每一个结点进行更新
  4. for i in range(layers):
  5. # 取w中对应的结点拼成卷积核
  6. weight = tf.concat([w[4 * num + i - 1] for num in range(i)] + [w[4 * num + i] for num in range(i + 1, layers)], axis=2)
  7. # 使用拼出的卷积核与最近更新的结点更新最早的结点
  8. data = conv2d(x, weight)
  9. # 对数据进行批归一化,加快网络训练,减轻梯度消失
  10. data = bn(data, train)
  11. # 使用ReLU激活函数使得模型非线性
  12. data = tf.nn.relu(data)
  13. # 使用dropout随机使部分结点归零,从而使得模型不易过拟合
  14. data = dropout(data, train)
  15. # 若不为更新最后一个结点
  16. if i != layers - 1:
  17. # 则将更新最早的结点去除
  18. _, x = tf.split(x, [filters, filters * (layers - 2)], axis=3)
  19. # 将最新更新的结点同其他结点concat在一起,对于非最后结点相当于替换最早更新的结点
  20. x = tf.concat([data, x], axis=3)
  21. # 返回经过stage2后的所有经过更新的结点
  22. return x

4.3 cliquenet.py中transition函数

将layer在channel层面做attention,使得整个layer都学习到上下文的信息。

  1. # 定义了在多个block之间的转换模块
  2. def transition(x, regularizer, in_channel, out_channel, size, train, use_attention=True):
  3. # 得到之后进行的1✖1卷积的卷积核张量,输入channel与输出channel为给定值,在该网络中由于较浅,因此暂不使用compression机制,输入channel与输出channel数相等
  4. w = get_weight([1, 1, in_channel, out_channel], 0.1, regularizer)
  5. # 根据上述卷积核进行卷积,步长为1,使得feature map维度不变
  6. x = conv2d(x, w, 1)
  7. # 对数据进行批归一化,加快网络训练,减轻梯度消失
  8. x = bn(x, train)
  9. # 使用ReLU激活函数使得模型非线性
  10. x = tf.nn.relu(x)
  11. # 使用dropout随机使部分结点归零,从而使得模型不易过拟合
  12. x = dropout(x, train)
  13. # 选择是否使用attention机制,这里采用了channel-wise的attention机制来在转化过程赋予不同channel不同权重,从而使得下一个block的学习效果更好
  14. if use_attention:
  15. # 对数据进行global pooling,从而对于对于每一个channel得到一个值
  16. attention = tf.nn.avg_pool(x, ksize=[1, size, size, 1], strides=[1, size, size, 1], padding='SAME')
  17. # 将得到的每一个张量转换为一个长度为channel数的长向量
  18. attention = tf.reshape(attention, [-1, out_channel])
  19. # 将获得的权重输入一个全连接层,从而对权重进行学习
  20. attention = fc(attention, out_channel, out_channel // 2, 0.01, regularizer)
  21. # 使用ReLU激活函数使得attention的学习模块非线性
  22. attention = tf.nn.relu(attention)
  23. # 将获得的权重输入一个全连接层,从而对权重进行更深的学习
  24. attention = fc(attention, out_channel // 2, out_channel, 0.01, regularizer)
  25. # 使用sigmoid激活函数使得学习到的权重处于0-1的范围内
  26. attention = tf.sigmoid(attention)
  27. # 增大张量维度以方便数据与attention的权重的相乘
  28. attention = tf.expand_dims(attention, 1)
  29. # 增大张量维度以方便数据与attention的权重的相乘
  30. attention = tf.expand_dims(attention, 1)
  31. # 将学习到的channel-wise attention权重分别乘以各自的channel
  32. x = tf.multiply(x, attention)
  33. # 对数据进行最大池化,使得feature map的维度减半,使得下一个block可以学习到更深层的信息
  34. x = tf.nn.avg_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
  35. # 返回经过transition转换后的数据,用于传入下一个block
  36. return x

5 结果分析

5.1 使用cliqueNet网络优点

增强information flow
减少内存
加速训练
避免网络退化
保证实时性

5.2 参数优化

进行了数据增强
使用了Validation调整优化器与超参数
比较了Adam、Momentum、SGD优化器,其中Momentum收敛较慢,SGD容易梯度消失。最终选择Adam优化器
Dropout = 0.95

5.3 实验环境

操作系统:Ubuntu 16.04LTS
GPU:GTX 1080TI
Python版本: Python3.5
TensorFlow版本:1.11.0

5.4 模型效果

fer2013 Rank最好成绩是0.71。以下是实验结果,最高准确率只有0.653.
截屏2020-12-23 下午4.55.23.png
真实数据测试如下,比如周星驰图片中的泪光不够明显,影响了模型的预测结果,得出结论:如何增加标志性特征的对比度是非常重要的。
截屏2020-12-23 下午4.56.51.png
以下是表情包的测试,图片中没有一个标准的人脸形状时,模型的准确率也不高。

截屏2020-12-23 下午4.57.02.png

5.5 总结

通过预训练的方式来找到较好的收敛特征。fine-tune:增光数据集上学习率较大的coarse预训练
使用图像分割的方式,矫正面部位置

6 源码下载

项目源码下载