在上一篇博文中,通过使用Python进行数据的清洗整理,我们最终获得了3个TensorFlow可以使用的分别用于训练、验证与测试的TFRecord文件。这三个文件标志着数据预处理工作的结束。在本篇博文中,我们将基于TensorFlow构建一种被称为“卷积神经网络”的深度学习模型。这种模型尤其适用于用在图像这种结构化数据的处理中。
卷积网络模型结构
在基于深度学习的人脸特征点检测 - 数据与方法这篇文章中我们提到过将来会采用一种深度卷积神经网络。按照文献中的描述,该网络的结构大致如下表所示。我额外增加了一些描述内容以及参数方便讨论。
LAYER NAME | CONTENT | NOTE |
---|---|---|
input | 128 × 128 × 3 | 输入层,3通道图像,大小128 × 128像素 |
conv1 | 3 × 3 × 32 | 卷积层,核3×3,stride 1 |
pool1 | 2 × 2, stride 2 | 池化层,Max pooling |
conv2 | 3 × 3 × 64 | 卷积层,核3×3,stride 1 |
conv3 | 3 × 3 × 64 | 卷积层,核3×3,stride 1 |
pool2 | 2 × 2, stride 2 | 池化层,Max pooling |
conv4 | 3 × 3 × 64 | 卷积层,核3×3,stride 1 |
conv5 | 3 × 3 × 64 | 卷积层,核3×3,stride 1 |
pool3 | 2 × 2, stride 1 | 池化层,Max pooling |
conv6 | 3 × 3 × 128 | 卷积层,核3×3,stride 1 |
conv7 | 3 × 3 × 128 | 卷积层,核3×3,stride 1 |
pool4 | 2 × 2, stride 1 | 池化层,Max pooling |
conv8 | 3 × 3 × 256 | 卷积层,核3×3,stride 1 |
dense1 | 1024 | 全连接层,大小1024 |
logits | 136 | 输出层,也是全连接层,大小136 |
该网络的输入为128 × 128的彩色图像。之后会经历一系列的卷积与池化操作。其中的卷积层是一种有效的二维特征提取方法,它能够有效的降低神经网络的参数数量,是图像处理中常用的层结构。池化层则在保留上一层中最响应强烈的特征同时进一步精简了参数数量。输出层为全连接层,直接输出68个面部特征点的坐标。
TensorFlow的使用策略
模型明确后,接下来就要动手写代码来构建它。这里我将采用TensorFlow,一个来自Google的开源深度学习框架。它的使用非常灵活,官方是这样介绍它的:
TensorFlow既提供了高层级的API以便让您轻松构建和训练您的模型,也提供了低层级的控制功能以尽可能提高灵活性和性能。
神经网络的训练过程不仅仅要建立网络模型,还需要考虑训练过程中的变量初始化、队列IO、运算资源分配、运算状态的保存与恢复、训练日志的读写等等。在初期,我推荐使用TensorFlow的Estimator
来辅助完成这些内容,这样我们可以把精力放在网络实现上。
Estimator的使用方法
在官方教程[1]里你可以找到Estimator的具体使用方法。概括的说,使用Estimator需要你完成以下内容:
- 定义自己的网络模型结构
- 定义输入函数
实现以上两项内容就可以使用Estimator愉快的开始训练了。
构建卷积神经网络
由于我们的模型采用了自定义的网络结构,因此我们需要从Layer层面开始构建。模型中共涉及到3种层类型,分别是卷积层、池化层和全连接层。
卷积层
TensorFlow提供了多种卷积层的实现方法,我推荐采用tf.layers.conv2d
来实现[2]。这样构建第一个卷积层时可以这样做:
conv1 = tf.layers.conv2d(
inputs=inputs,
filters=32,
kernel_size=[3, 3],
strides=(1, 1),
padding='valid',
activation=tf.nn.relu)
其中filters
是指卷积层输出的特征图个数;kernel_size
是卷积核的大小;strids
是偏移量;padding
则表明在卷积时原图是否需要扩展;activation
是指激活函数,这里我选择了ReLU[3]。
池化层
池化层同样可以采用layer层面的函数tf.layers.max_pooling2d
:
pool1 = tf.layers.max_pooling2d(
inputs=conv1,
pool_size=[2, 2],
strides=(2, 2),
padding='valid')
其中pool_size
是指降采样的尺度;strides
同样是偏移量;padding
同上。
全连接层
全连接层是最基本的层类型,该层的每一个神经元都会与上一层的所有神经元相连接,而不会像卷积层一样存在参数共享的状况。全连接是一种“稠密”的连接方式,使用函数tf.layers.dense
来实现[4]:
logits = tf.layers.dense(
inputs=dense1,
units=136,
activation=None,
use_bias=True,
name="logits")
其中的units
是指神经元的个数;这里我指定了name
属性,为了方便将来使用。
其它
在全连接之前,需要把二维的layer转换为一维的。这里我推荐使用tf.layers.flatten
[5]。除了这些,TensorFlow还提供了其它众多层类型可以使用,例如tf.layers.dropout
可以防止过拟合[6];tf.layers.batch_normalization
可以解决因非线性饱和效应造成的训练迟缓[7]。在这里我决定先从最简单的情况做起,暂不采用。
通过不断的“堆叠”上述函数可以建立一个完整的神经网络模型。受限于篇幅,我就不在正文中贴出完整的代码了。您可以去我的Github上去查阅完整的代码。
训练结果
由于训练样本的数量达到了20万张,我推荐使用GPU版本的TensorFlow进行计算。经过大约一个小时的训练,我尝试将测试数据打印出来检查,发现一个有趣的现象:输出值中存在负数。但我所提供的训练数据全部为正数。如果将Mark点绘制到图像上,则会出现超出图像范围的现象。但是仔细观察发现Mark点的轮廓依旧是面部形状。所以尝试手工对位置进行纠正,得到了以下结果:
看起来神经网络是学到了一种规律:特征点的排布呈现人脸的形状。但是它似乎没有学到特征点的绝对位置关系,特征点都呈现出程度不一的整体偏移,不过这也不排除是我手动修正造成的。我寄希望于更多的训练可以继续获得更好的结果,谁知好景不长,在训练到130k setp的时候,loss发生了这样一种变化:
至于输出的特征点...
无论输入是什么,它都认准一个输出。看来是掉到某个坑里出不来了。问题在于,错误出在哪里?
loss函数惹的祸
通过对代码仔细排查,终于定位到了问题点:loss函数用错了。
我打算使用均方差作为最终loss的判定依据,但是在实际的代码中我使用了函数tf.losses.mean_pairwise_squared_error
。关于这个函数官方的解释如下:
For example, if labels=[a, b, c] and predictions=[x, y, z], there are three pairs of differences are summed to compute the loss: loss = [ ((a-b) - (x-y)).^2 + ((a-c) - (x-z)).^2 + ((b-c) - (y-z)).^2 ] / 3
由于选择了错误的函数,导致神经网络在最小化loss的时候,将点与点之间的关系作为了优化的依据。也就是说,它学习的目的被我错误的设定为样本内部点与点之间的联系。
修正loss函数
使用正确的loss函数tf.losses.mean_squared_error
后我重新训练了神经网络。新的loss下降曲线如下图:
看上去没啥大的毛病,在测试集上跑跑看~
看上去略完美!不过,在真实情况中会怎样呢?
Face in the wild!
为了检验这个神经网络的实际表现,我用搜索引擎手动随机抓取了6张图片。这6张图片是神经网络从来都没有见过的面部图像!按照之前数据处理的方法提取出面部区域并送入神经网络,效果如下:
偏差很大!
为什么?
-
https://tensorflow.google.cn/programmers_guide/estimators ↩︎
-
https://tensorflow.google.cn/api_docs/python/tf/layers/conv2d ↩︎
-
https://en.wikipedia.org/wiki/Rectifier_(neural_networks) ↩︎
-
https://tensorflow.google.cn/api_docs/python/tf/layers/dense ↩︎
-
https://tensorflow.google.cn/api_docs/python/tf/layers/flatten ↩︎
-
https://tensorflow.google.cn/api_docs/python/tf/layers/dropout ↩︎
-
https://tensorflow.google.cn/api_docs/python/tf/layers/batch_normalization ↩︎
-