用卷积神经网络检测脸部关键点的教程(二)卷积神经网络训练和数据扩充

该教程介绍了使用卷积神经网络(CNN)进行脸部关键点检测,包括CNN的训练过程和数据扩充技术。通过Lasagne库构建CNN模型,利用数据扩充增加训练集多样性,如水平翻转,以提升模型性能。经过训练,CNN模型相比于单层神经网络,表现出更好的预测效果。
摘要由CSDN通过智能技术生成

本文翻译自Using convolutional neural nets to detect facial keypoints tutorial

用卷积神经网络检测脸部关键点的教程(一)环境配置与浅层网络训练
用卷积神经网络检测脸部关键点的教程(二)卷积神经网络训练和数据扩充
用卷积神经网络检测脸部关键点的教程(三)学习率,学习动量,dropout
用卷积神经网络检测脸部关键点的教程(四)通过前训练(pre-train)训练专项网络

上一次我们用了单隐层的神经网络,效果还可以改善,这一次就使用CNN。

卷积神经网络

这里写图片描述

上图演示了卷积操作

LeNet-5式的卷积神经网络,是计算机视觉领域近期取得的巨大突破的核心。卷积层和之前的全连接层不同,采用了一些技巧来避免过多的参数个数,但保持了模型的描述能力。这些技巧是:
1, 局部联结:神经元仅仅联结前一层神经元的一小部分。
2, 权重共享:在卷积层,神经元子集之间的权重是共享的。(这些神经元的形式被称为特征图[feature map])
3, 池化:对输入进行静态的子采样。

这里写图片描述
局部性和权重共享的图示

卷积层的单元实际上连接了前一层神经元中的一个2维patch,这个前提让网络利用了输入中的2维结构。

当使用Lasagne中的卷积层时,我们必须进行一些输入准备。输入不再像刚刚一样是一个9216像素强度的扁平向量,而是一个有着(c,0,1)形式的三维矩阵,其中c代表通道(颜色),0和1对应着图像的x和y维度。在我们的问题中,具体的三维矩阵为(1,96,96),因为我们仅仅使用了灰度一个颜色通道。

一个函数load2d对前述的load函数进行了包装,完成这个2维到三维的转变:

def load2d(test=False, cols=None):
    X, y = load(test=test)
    X = X.reshape(-1, 1, 96, 96)
    return X, y

我们将要创建一个具有三个卷积层和两个全连接层的卷积神经网络。每个卷积层都跟着一个2*2的最大化池化层。初始卷积层有32个filter,之后每个卷积层我们把filter的数量翻番。全连接的隐层包含500个神经元。

这里还是一样没有任何形式(惩罚权重或者dropout)的正则化。事实证明当我们使用尺寸非常小的filter,如3*3或2*2,已经起到了非常不错的正则化效果。

代码如下:

net2 = NeuralNet(
    layers=[
        ('input', layers.InputLayer),
        ('conv1', layers.Conv2DLayer),
        ('pool1', layers.MaxPool2DLayer),
        ('conv2', layers.Conv2DLayer),
        ('pool2', layers.MaxPool2DLayer),
        ('conv3', layers.Conv2DLayer),
        ('pool3', layers.MaxPool2DLayer),
        ('hidden4', layers.DenseLayer),
        ('hidden5', layers.DenseLayer),
        ('output', layers.DenseLayer),
        ],
    input_shape=(None, 1, 96, 96),
    conv1_num_filters=32, conv1_filter_size=(3, 3), pool1_pool_size=(2, 2),
    conv2_num_filters=64, conv2_filter_size=(2, 2), pool2_pool_size=(2, 2),
    conv3_num_filters=128, conv3_filter_size=(2, 2), pool3_pool_size=(2, 2),
    hidden4_num_units=500, hidden5_num_units=500,
    output_num_units=30, output_nonlinearity=None,

    update_learning_rate=0.01,
    update_momentum=0.9,

    regression=True,
    max_epochs=1000,
    verbose=1,
    )

X, y = load2d()  # load 2-d data
net2.fit(X, y)

# Training for 1000 epochs will take a while.  We'll pickle the
# trained model so that we can load it back later:
import cPickle as pickle
with open('net2.pickle', 'wb') as f:
    pickle.dump(net2, f, -1)

训练这个网络和第一个网络相比,将要耗费巨大的时空资源。每次迭代要慢15倍,整个1000次迭代下来要耗费20多分钟的时间,这还是在你有一个相当不错的GPU的基础上。

然而耐心总是得到回馈,我们的模型和结果自然比刚刚好得多。让我们来看一看运行脚本时的输出。首先是输出形状的层列表,注意因为我们选择的窗口尺寸,第一个卷积层的32个filter输出了32张94*94 的特征图。

InputLayer            (None, 1, 96, 96)       produces    9216 outputs
Conv2DCCLayer         (None, 32, 94, 94)      produces  282752 outputs
MaxPool2DCCLayer      (None, 32, 47, 47)      produces   70688 outputs
Conv2DCCLayer         (None, 64, 46, 46)      produces  135424 outputs
MaxPool2DCCLayer      (None, 64, 23, 23)      produces   33856 outputs
Conv2DCCLayer         (None, 128, 22, 22)     produces   61952 outputs
MaxPool2DCCLayer      (None, 128, 11, 11)     produces   15488 outputs
DenseLayer            (None, 500)             produces     500 outputs
DenseLayer            (None, 500)             produces     500 outputs
DenseLayer            (None, 30)              produces      30 outputs

接下来我们看到,和第一个网络输出相同,是每一次迭代训练损失和验证损失以及他们之间的比率。

Epoch  |  Train loss  |  Valid loss  |  Train / Val
--------|--------------|--------------|----------------
     1  |    0.111763  |    0.042740  |     2.614934
     2  |    0.018500  |    0.009413  |     1.965295
     3  |    0.008598  |    0.007918  |     1.085823
     4  |    0.007292  |    0.007284  |     1.001139
     5  |    0.006783  |    0.006841  |     0.991525
...
   500  |    0.001791  |    0.002013  |     0.889810
   501  |    0.001789  |    0.002011  |     0.889433
   502  |    0.001786  |    0.002009  |     0.889044
   503  |    0.001783  |    0.002007  |     0.888534
   504  |    0.001780  |    0.002004  |     0.888095
   505  |    0.001777  |    0.002002  |     0.887699
...
   995  |    0.001083  |    0.001568  |     0.690497
   996  |    0.001082  |    0.001567  |     0.690216
   997  |    0.001081  |    0.001567  |     0.689867
   998  |    0.001080  |    0.001567  |     0.689595
   999  |    0.001080  |    0.001567  |     0.689089
  1000  |    0.001079  |    0.001566  |     0.688874

1000次迭代后的结果相对第一个网络,有了非常不错的改善,我们的RMSE也有不错的结果。

>>> np.sqrt(0.001566) * 48
1.8994904579913006

我们从测试集合里面取同一个样例,画出两个网络的预测结果来进行对比:

sample1 = load(test=True)[0][6:7]
sample2 = load2d(test=True)[0][6:7]
y_pred1 = net1.predict(sample1)[0]
y_pred2 = net2.predict(sample2)[0]

fig = pyplot.figure(figsize=(6, 3))
ax = fig.add_subplot(1, 2, 1, xticks=[], yticks=[])
plot_sample(sample1[0], y_pred1, ax)
ax = fig.add_subplot(1, 2, 2, xticks=[], yticks=[])
plot_sample(sample1[0], y_pred2, ax)
pyplot.show()

这里写图片描述
左边的net1的预测与net2的预测相比。

然后让我们比较第一和第二网络的学习曲线:

这看起来不错,我喜欢新的误差曲线的平滑度。 但是我们注意到,到最后,net2的验证错误比训练错误更快地趋于水平。 我敢打赌,我们可以通过使用更多的培训示例来改善。 如果我们水平翻转输入图像怎么办? 我们能够通过使这样的培训数据量增加一倍来改进培训吗?

数据扩充

通常情况下,增加训练集的数量会让一个过拟合的网络取得更好的训练结果。(如果你的网络没有过拟合,那么你最好把它变大。(否则不足以描述数据))
数据扩充的意思是我们人为地通过一些手段(变形、添加噪声等等)增加训练用例的个数。显然,这要比手工收集更多的样本经济许多。所以,数据扩充是深度学习工具箱里必不可少的工具。

我们曾经在前面简短的提到批处理迭代器。批量迭代器的工作是采集一个训练集合的样本矩阵,分成不同的批次(在我们的任务128个用例一批)。当把训练样本分成批次的时候,批处理迭代器可以顺便把输入变形这件事做的又快又好。所以当我们想要进行水平翻转的时候,我们不需要去翻倍数量巨大的训练集合。更好的方法是,我们在进行批处理迭代的时候,以50%的几率进行水平翻转就可以了。这非常的方便,对某些问题来说这种手段可以让我们生产近乎无限的训练集,而不需要增加内存的使用。同时,输入图片的变形操作可以在GPU进行上一个批次运算的时候进行,所以可以说,这个操作几乎不增加任何额外的资源消耗。

水平翻转图片事实上仅仅是矩阵的切片操作:

X, y = load2d()
X_flipped = X[:, :, :, ::-1]  # simple slice to flip all images

# plot two images:
fig = pyplot.figure(figsize=(6, 3))
ax = fig.add_subplot(1, 2, 1, xticks=[], yticks=[])
plot_sample(X[1], y[1], ax)
ax = fig.add_subplot(1, 2, 2, xticks=[], yticks=[])
plot_sample(X_flipped[1], y[1], ax)
pyplot.show()

这里写图片描述
原始图片(左)和翻转图片

在右边的图片中,值得注意的是关键点的位置并不匹配。因为我们翻转了图片,所以我们也必须翻转目标位置的横坐标,同时,还要交换目标值的位置,因为left_eye_center_x 变换过之后的值,指示的实际上是right_eye_center_x。我们建立一个元组flip_indices来保存哪一列的目标向量需要交换位置。如果你还记得,最开始的时候我们读取的数据条数是这样的:

left_eye_center_x 7034
left_eye_center_y 7034
right_eye_center_x 7032
right_eye_center_y 7032
left_eye_inner_corner_x 2266
left_eye_inner_corner_y 2266

因为left_eye_center_x要和right_eye_center_x换位置,我们记录(0,2),同样left_eye_center_y要和right_eye_center_y换位置,我们记录元组(1,3),以此类推。最后,我们获得元组集合如下:

flip_indices = [
    (0, 2), (1, 3),
    (4, 8), (5, 9), (6, 10), (7, 11),
    (12, 16), (13, 17), (14, 18), (15, 19),
    (22, 24), (23, 25),
    ]

# Let's see if we got it right:
df = read_csv(os.path.expanduser(FTRAIN))
for i, j in flip_indices:
    print("# {} -> {}".format(df.columns[i], df.columns[j]))

# this prints out:
# left_eye_center_x -> right_eye_center_x
# left_eye_center_y -> right_eye_center_y
# left_eye_inner_corner_x -> right_eye_inner_corner_x
# left_eye_inner_corner_y -> right_eye_inner_corner_y
# left_eye_outer_corner_x -> right_eye_outer_corner_x
# left_eye_outer_corner_y -> right_eye_outer_corner_y
# left_eyebrow_inner_end_x -> right_eyebrow_inner_end_x
# left_eyebrow_inner_end_y -> right_eyebrow_inner_end_y
# left_eyebrow_outer_end_x -> right_eyebrow_outer_end_x
# left_eyebrow_outer_end_y -> right_eyebrow_outer_end_y
# mouth_left_corner_x -> mouth_right_corner_x
# mouth_left_corner_y -> mouth_right_corner_y

我们的批处理迭代器的实现将会从BachIterator类派生,重载transform()方法。把这些东西组合到一起,看看完整的代码:

from nolearn.lasagne import BatchIterator

class FlipBatchIterator(BatchIterator):
    flip_indices = [
        (0, 2), (1, 3),
        (4, 8), (5, 9), (6, 10), (7, 11),
        (12, 16), (13, 17), (14, 18), (15, 19),
        (22, 24), (23, 25),
        ]

    def transform(self, Xb, yb):
        Xb, yb = super(FlipBatchIterator, self).transform(Xb, yb)

        # Flip half of the images in this batch at random:
        bs = Xb.shape[0]
        indices = np.random.choice(bs, bs / 2, replace=False)
        Xb[indices] = Xb[indices, :, :, ::-1]

        if yb is not None:
            # Horizontal flip of all x coordinates:
            yb[indices, ::2] = yb[indices, ::2] * -1

            # Swap places, e.g. left_eye_center_x -> right_eye_center_x
            for a, b in self.flip_indices:
                yb[indices, a], yb[indices, b] = (
                    yb[indices, b], yb[indices, a])

        return Xb, yb

使用上述批处理迭代器进行训练,需要把它作为batch_iterator_train参数传递给NeuralNet。让我们来定义net3,一个和net2非常相似的网络。仅仅是在网络的最后添加了这些行:

net3 = NeuralNet(
    # ...
    regression=True,
    batch_iterator_train=FlipBatchIterator(batch_size=128),
    max_epochs=3000,
    verbose=1,
    )

现在我们已经采用了最新的翻转技巧,但同时我们将迭代次数增长了三倍。因为我们并没有真正改变训练集合的总数,所以每个epoch仍然使用和刚刚一样多的样本个数。事实证明,采用了新技巧后,每个训练epoch还是比刚刚多用了一些时间。这次我们的网络学到的东西更具一般性,理论上来讲学习更一般性的规律比学出过拟合总是要更难一些。

这次网络将花费一小时的训练时间,我们要确保在训练之后,把得到的模型保存起来。然后就可以去喝杯茶或者做做家务活,洗衣服也是不错的选择。

net3.fit(X, y)

import cPickle as pickle
with open('net3.pickle', 'wb') as f:
    pickle.dump(net3, f, -1)
$ python kfkd.py
...
 Epoch  |  Train loss  |  Valid loss  |  Train / Val
--------|--------------|--------------|----------------
...
   500  |    0.002238  |    0.002303  |     0.971519
...
  1000  |    0.001365  |    0.001623  |     0.841110
  1500  |    0.001067  |    0.001457  |     0.732018
  2000  |    0.000895  |    0.001369  |     0.653721
  2500  |    0.000761  |    0.001320  |     0.576831
  3000  |    0.000678  |    0.001288  |     0.526410

让我们画出学习曲线和net2对比。应该看到3000次迭代后的效果,net3比net2的验证损失要小了5%。我们看到在2000次迭代之后,net2已经停止学习了,并且曲线变得不光滑;但是net3的效果却在一直改进,尽管进展缓慢。

这里写图片描述

看起来费了很大的劲却只取得了一点点加成,是这样吗?我们将在下一部分找到答案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值