1 概述
任务目标:人脸表情识别
数据来源:kaggle
模型设计:Lenet7和CliqueNet
度量标准:准确率
实验分类:8类(愤怒 恶心 害怕 快乐 悲伤 惊讶 蔑视 面无表情)
2 数据集
2.1 数据集下载
由Microsoft对kaggle提出的数据集进行重新标注的数据集
数据集下载
2.2 数据规模
训练集:测试集:验证集 = 8:1:1
1P = 48 * 48 = 2304像素
2.3 数据格式
-csv -3个属性:Label Pixels Usage
实际内容:人脸(灰度)
8中表情:愤怒 恶心 害怕 快乐 悲伤 惊讶 蔑视 面无表情(No Expression)
面部基本居中,大小相似
预处理数据下载
3 模型说明
3.1 Lenet7
Lenet7原来的网络使用两层卷积层和两层池化层,在实践中增大了模型深度,更好的适应当前数据特征
增加一个卷积和池化层
增加一层全连接层。
3.2 CliqueNet
(1)简介
由DenseNet启发
各Layer间双向连接
stage1: 浅层—>高层特征
stage2: 近层更新远层
(2)网络结构
本项目中由于数据集复杂程度不高,使用l三个block,每个block提取特征进行预测。浅层提取到了细节特征,深层提取到了全局信息。
此外在网络中使用Transition模块,使用channel-wise attention来给不同的channel赋予不同的权重。
4 代码分析
4.1 cliquenet.py中forward函数
其中block模块主要对一维和高维图像特征进行提取
transition模块主要通过channel-wise attention机制对cannel的结构进行优化,以使得channel都获得所有维度的特征信息,有益于后面过程的学习。
# 定义了cliquenet的前向传播函数
def forward(x, train=True, regularizer=None):
# 得到之后进行卷积的卷积核张量,输入channel为1,输出channel为64,kernel size为3
w = get_weight([3, 3, 1, 64], 0.1, regularizer)
# 先进行一次卷积,步长为2,使得feature map维度减半,使得模型提取到浅层特征
x = conv2d(x, w, 2)
# 对数据进行批归一化,加快网络训练,减轻梯度消失
x = bn(x, train)
# 使用ReLU激活函数使得模型非线性
x = tf.nn.relu(x)
# 对数据进行最大池化,使得feature map的维度减半
x = tf.nn.max_pool(x, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
## block1
# 将数据送入clique block,block内每一个结点的channel数设为36,一个block内除输入结点外有5个结点
x, feature1 = clique_block(x, regularizer, 64, 36, 3, 5, train)
# 对第一个block提取到的特征信息进行global pooling
feature1 = tf.nn.avg_pool(feature1, ksize=[1, 12, 12, 1], strides=[1, 12, 12, 1], padding='SAME')
# 将上一层的五个更新后结点作为输入,经过transition调整feature map的维度,并利用attention强化特征信息更优的channel,使得下一个block可以更好地利用前层特征信息
x = transition(x, regularizer, 180, 180, 12, train)
## block 2
# 将数据送入clique block,block内每一个结点的channel数设为36,一个block内除输入结点外有5个结点
x, feature2 = clique_block(x, regularizer, 180, 36, 3, 5, train)
# 对第二个block提取到的特征信息进行global pooling
feature2 = tf.nn.avg_pool(feature2, ksize=[1, 6, 6, 1], strides=[1, 6, 6, 1], padding='SAME')
# 将上一层的五个更新后结点作为输入,经过transition调整feature map的维度,并利用attention强化特征信息更优的channel,使得下一个block可以更好地利用前层特征信息
x = transition(x, regularizer, 180, 180, 6, train)
## block3
# 将数据送入clique block,block内每一个结点的channel数设为36,一个block内除输入结点外有5个结点
_, feature3 = clique_block(x, regularizer, 180, 36, 3, 5, train)
# 对第三个block提取到的特征信息进行global pooling
feature3 = tf.nn.avg_pool(feature3, ksize=[1, 3, 3, 1], strides=[1, 3, 3, 1], padding='SAME')
# 将不同层间获取的不同特征进行融合,浅层的特征包含更多的细节信息,而深层特征则包含更多的全局信息
out = tf.concat([feature1, feature2, feature3], axis=3)
# 将张量转换为一个长向量,以用于之后的全连接层
x = tf.reshape(out, [-1, 964])
# 将向量输入全连接层,从而输出对每一个分类的预测分数
x = fc(x, 964, 7, 0.01, regularizer)
# 返回该神经网络的预测结果
return x
4.2 cliquenet.py中stage1和stage2函数
stage1的公式
代码结构是
layer1 = encoding(input)
layer2 =encoding(layer1,input)
layer3 = encoding(layer2,input)
…
layer5 = encoding(layer4,input)
因此layer5中存在提取的高维提取特征,并且有很多重复的获取的低维特征。以使得低维和高维的特征共同被利用,加强了information flow,能有效避免梯度消失。
# 定义了stage1阶段
def stage1(x0, w0, w, in_channel, filters, layers=5, train=True):
# 使用输入结点对每一个之外的结点进行初始化
for i in range(layers):
# 如果是第一个结点
if i == 0:
# 就取w0中的第一个张量作为连接输入结点与第一个结点的卷积核
weight = w0[i]
# 第一个结点在stage1只与输入结点相连接
data = x0
# 如果不是第一个结点
else:
# 就取w0中对应的结点以及w中对应的结点拼成之后使用的卷积核
weight = tf.concat([w0[i]] + [w[4 * num + i - 1] for num in range(i)], axis=2)
# 使用拼出的卷积核与前面的结点生成一个新的结点
x = conv2d(data, weight)
# 对数据进行批归一化,加快网络训练,减轻梯度消失
x = bn(x, train)
# 使用ReLU激活函数使得模型非线性
x = tf.nn.relu(x)
# 使用dropout随机使部分结点归零,从而使得模型不易过拟合
x = dropout(x, train)
# 将已经生成的结点concat在一起用于生成下一个结点
data = tf.concat([data, x], axis=3)
# 除去输入结点与第一个结点(第一个结点在stage2的第一步即需要被更新,因此不需要传入下一阶段)
_, x = tf.split(data, [in_channel + filters, filters * (layers - 1)], axis=3)
# 将得到的后面若干个结点传入stage2
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)
# 定义了stage2模块
def stage2(x, w, in_channel, filters, layers=5, train=True):
# 对block内每一个结点进行更新
for i in range(layers):
# 取w中对应的结点拼成卷积核
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)
# 使用拼出的卷积核与最近更新的结点更新最早的结点
data = conv2d(x, weight)
# 对数据进行批归一化,加快网络训练,减轻梯度消失
data = bn(data, train)
# 使用ReLU激活函数使得模型非线性
data = tf.nn.relu(data)
# 使用dropout随机使部分结点归零,从而使得模型不易过拟合
data = dropout(data, train)
# 若不为更新最后一个结点
if i != layers - 1:
# 则将更新最早的结点去除
_, x = tf.split(x, [filters, filters * (layers - 2)], axis=3)
# 将最新更新的结点同其他结点concat在一起,对于非最后结点相当于替换最早更新的结点
x = tf.concat([data, x], axis=3)
# 返回经过stage2后的所有经过更新的结点
return x
4.3 cliquenet.py中transition函数
将layer在channel层面做attention,使得整个layer都学习到上下文的信息。
# 定义了在多个block之间的转换模块
def transition(x, regularizer, in_channel, out_channel, size, train, use_attention=True):
# 得到之后进行的1✖1卷积的卷积核张量,输入channel与输出channel为给定值,在该网络中由于较浅,因此暂不使用compression机制,输入channel与输出channel数相等
w = get_weight([1, 1, in_channel, out_channel], 0.1, regularizer)
# 根据上述卷积核进行卷积,步长为1,使得feature map维度不变
x = conv2d(x, w, 1)
# 对数据进行批归一化,加快网络训练,减轻梯度消失
x = bn(x, train)
# 使用ReLU激活函数使得模型非线性
x = tf.nn.relu(x)
# 使用dropout随机使部分结点归零,从而使得模型不易过拟合
x = dropout(x, train)
# 选择是否使用attention机制,这里采用了channel-wise的attention机制来在转化过程赋予不同channel不同权重,从而使得下一个block的学习效果更好
if use_attention:
# 对数据进行global pooling,从而对于对于每一个channel得到一个值
attention = tf.nn.avg_pool(x, ksize=[1, size, size, 1], strides=[1, size, size, 1], padding='SAME')
# 将得到的每一个张量转换为一个长度为channel数的长向量
attention = tf.reshape(attention, [-1, out_channel])
# 将获得的权重输入一个全连接层,从而对权重进行学习
attention = fc(attention, out_channel, out_channel // 2, 0.01, regularizer)
# 使用ReLU激活函数使得attention的学习模块非线性
attention = tf.nn.relu(attention)
# 将获得的权重输入一个全连接层,从而对权重进行更深的学习
attention = fc(attention, out_channel // 2, out_channel, 0.01, regularizer)
# 使用sigmoid激活函数使得学习到的权重处于0-1的范围内
attention = tf.sigmoid(attention)
# 增大张量维度以方便数据与attention的权重的相乘
attention = tf.expand_dims(attention, 1)
# 增大张量维度以方便数据与attention的权重的相乘
attention = tf.expand_dims(attention, 1)
# 将学习到的channel-wise attention权重分别乘以各自的channel
x = tf.multiply(x, attention)
# 对数据进行最大池化,使得feature map的维度减半,使得下一个block可以学习到更深层的信息
x = tf.nn.avg_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# 返回经过transition转换后的数据,用于传入下一个block
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.
真实数据测试如下,比如周星驰图片中的泪光不够明显,影响了模型的预测结果,得出结论:如何增加标志性特征的对比度是非常重要的。
以下是表情包的测试,图片中没有一个标准的人脸形状时,模型的准确率也不高。
5.5 总结
通过预训练的方式来找到较好的收敛特征。fine-tune:增光数据集上学习率较大的coarse预训练
使用图像分割的方式,矫正面部位置