用卷积神经网络检测脸部关键点的教程(三)学习率,学习动量,dropout

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

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

上个模型令人讨厌的地方是光训练就花了一个小时的时间,等结果并不是一个令人心情愉快的事情。这一部分,我们将讨论将两个技巧结合让网络训练的更快!

直觉上的解决办法是,开始训练时取一个较高的学习率,随着迭代次数的增多不停的减小这个值。这是有道理的,因为开始的时候我们距离全局最优点非常远,我们想要朝着最优点的方向大步前进;然而里最优点越近,我们就前进的越谨慎,以免一步跨过去。举个例子说就是你乘火车回家,但你进家门的时候肯定是走进去,不能让火车开进去。

关于深度学习中的初始化和动量的重要性是Ilya Sutskever等人的谈话和论文的标题。在那里,我们学习了另一个有用的技巧来促进深度学习:即在训练期间增加优化方法的动量参数。

在我们以前的模型中,我们将学习率和学习势初始化为静态的0.01和0.9。让我们来改变这两个参数,使得学习率随着迭代次数线性减小,同时让学习动量增大。

NeuralNet允许我们在训练时通过on_epoch_finished函数来更新参数。于是我们传一个函数给on_epoch_finished,使得这个函数在每次迭代之后被调用。然而,在我们改变学习率和学习势这两个参数之前,我们必须将这两个参数改变为Theano shared variables。好在这非常简单。

import theano

def float32(k):
    return np.cast['float32'](k)

net4 = NeuralNet(
    # ...
    update_learning_rate=theano.shared(float32(0.03)),
    update_momentum=theano.shared(float32(0.9)),
    # ...
    )

我们传递的回调函数或者回调列表在调用时需要两个参数:nn,它是NeuralNet的实例;train_history,它和nn.history是同一个值。

不使用硬编码值的毁掉函数,我们将使用一个可参数化的类,在其中定义一个call函数来作为我们的回调函数。让我们把这个类叫做AdjustVariable,实现是相当简单的:

class AdjustVariable(object):
    def __init__(self, name, start=0.03, stop=0.001):
        self.name = name
        self.start, self.stop = start, stop
        self.ls = None

    def __call__(self, nn, train_history):
        if self.ls is None:
            self.ls = np.linspace(self.start, self.stop, nn.max_epochs)

        epoch = train_history[-1]['epoch']
        new_value = float32(self.ls[epoch - 1])
        getattr(nn, self.name).set_value(new_value)

现在让我们把这些变化放到一起,并开始准备训练网络:

net4 = NeuralNet(
    # ...
    update_learning_rate=theano.shared(float32(0.03)),
    update_momentum=theano.shared(float32(0.9)),
    # ...
    regression=True,
    # batch_iterator_train=FlipBatchIterator(batch_size=128),
    on_epoch_finished=[
        AdjustVariable('update_learning_rate', start=0.03, stop=0.0001),
        AdjustVariable('update_momentum', start=0.9, stop=0.999),
        ],
    max_epochs=3000,
    verbose=1,
    )

X, y = load2d()
net4.fit(X, y)

with open('net4.pickle', 'wb') as f:
    pickle.dump(net4, f, -1)

我们将训练两个网络:net4不使用我们的FlipBatchIterator,net5采用了。 除此之外,他们是相同的。

这是net4的学习:

Epoch  | Train loss | Valid loss | Train / Val
--------|--------------|--------------|----------------
    50  | 0.004216 | 0.003996 | 1.055011
   100  | 0.003533 | 0.003382 | 1.044791
   250  | 0.001557 | 0.001781 | 0.874249
   500  | 0.000915 | 0.001433 | 0.638702
   750  | 0.000653 | 0.001355 | 0.481806
  1000  | 0.000496 | 0.001387 | 0.357917

酷,训练发生得更快了! 在我们调整学习速度和学习动量之前,在500代和1000代的训练误差是以前在net2中的一半。 这一次,泛化程度似乎已经在750个左右的时期之后停止改善; 看起来没有什么意义的培训更长。

net5用了数据扩充之后怎么样?

poch  | Train loss | Valid loss | Train / Val
--------|--------------|--------------|----------------
    50  | 0.004317 | 0.004081 | 1.057609
   100  | 0.003756 | 0.003535 | 1.062619
   250  | 0.001765 | 0.001845 | 0.956560
   500  | 0.001135 | 0.001437 | 0.790225
   750  | 0.000878 | 0.001313 | 0.668903
  1000  | 0.000705 | 0.001260 | 0.559591
  1500  | 0.000492 | 0.001199 | 0.410526
  2000  | 0.000373 | 0.001184 | 0.315353

再次,我们有比net3更快的训练,更好的结果。在1000次迭代之后,结果比net3迭代了3000次的效果还要好。 此外,使用数据扩充训练的模型现在比没有数据扩充的模型好约10%。

丢弃技巧(Dropout)

2012年,在通过防止特征探测器的共适应来改进神经网络论文中引入了dropout,它是一种流行的正则化技术,工作非常好。我不会深入了解它为什么这么好的细节,你可以在别的地方读到。

像任何其他正则化技术一样,如果我们有一个过度拟合的网络,dropout才有意义,这在上一节中我们训练的net5网络显然是这样。 重要的是要记住,让你的网络训练得很好,首先过拟合,然后正则化。

要在Lasagne中使用dropout,我们将在现有图层之间添加DropoutLayer图层,并为每个图层指定退出概率。 这里是我们新网的完整定义。我在这些行的末尾添加了一个#!,用于区分和net5的不同。

net6 = NeuralNet(
    layers=[
        ('input', layers.InputLayer),
        ('conv1', layers.Conv2DLayer),
        ('pool1', layers.MaxPool2DLayer),
        ('dropout1', layers.DropoutLayer),  # !
        ('conv2', layers.Conv2DLayer),
        ('pool2', layers.MaxPool2DLayer),
        ('dropout2', layers.DropoutLayer),  # !
        ('conv3', layers.Conv2DLayer),
        ('pool3', layers.MaxPool2DLayer),
        ('dropout3', layers.DropoutLayer),  # !
        ('hidden4', layers.DenseLayer),
        ('dropout4', layers.DropoutLayer),  # !
        ('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),
    dropout1_p=0.1,  # !
    conv2_num_filters=64, conv2_filter_size=(2, 2), pool2_pool_size=(2, 2),
    dropout2_p=0.2,  # !
    conv3_num_filters=128, conv3_filter_size=(2, 2), pool3_pool_size=(2, 2),
    dropout3_p=0.3,  # !
    hidden4_num_units=500,
    dropout4_p=0.5,  # !
    hidden5_num_units=500,
    output_num_units=30, output_nonlinearity=None,

    update_learning_rate=theano.shared(float32(0.03)),
    update_momentum=theano.shared(float32(0.9)),

    regression=True,
    batch_iterator_train=FlipBatchIterator(batch_size=128),
    on_epoch_finished=[
        AdjustVariable('update_learning_rate', start=0.03, stop=0.0001),
        AdjustVariable('update_momentum', start=0.9, stop=0.999),
        ],
    max_epochs=3000,
    verbose=1,
    )

我们的网路现在已经大到可以让Python报一个“超过最大递归限制”错误了,所以为了避免这一点,我们最好增加python的递归限制。

import sys
sys.setrecursionlimit(10000)

X, y = load2d()
net6.fit(X, y)

import cPickle as pickle
with open('net6.pickle', 'wb') as f:
    pickle.dump(net6, f, -1)

看一下我们现在的训练,我们注意到训练速度又变慢了,以为添加了dropout,这是不出意料的效果。然而,整个网络的表现事实上超过了net5:

Epoch  | Train loss | Valid loss | Train / Val
--------|--------------|--------------|---------------
    50  | 0.004619 | 0.005198 | 0.888566
   100  | 0.004369 | 0.004182 | 1.044874
   250  | 0.003821 | 0.003577 | 1.068229
   500  | 0.002598 | 0.002236 | 1.161854
  1000  | 0.001902 | 0.001607 | 1.183391
  1500  | 0.001660 | 0.001383 | 1.200238
  2000  | 0.001496 | 0.001262 | 1.185684
  2500  | 0.001383 | 0.001181 | 1.171006
  3000  | 0.001306 | 0.001121 | 1.164100

过拟合也似乎没有那么糟糕。虽然我们必须小心这些数字:训练错误和验证错误之间的比率现在有一个稍微不同的意义,因为训练错误评估与遗漏,而验证错误评估没有遗漏。训练错误的更有价值的值是

from sklearn.metrics import mean_squared_error
print mean_squared_error(net6.predict(X), y)
# prints something like 0.0010073791

在我们以前的没有dropout的模型中,训练上的误差为0.000373。 所以不仅我们的dropout网表现略微好一点,它的过拟合也比我们以前的模型少得多。 这是个好消息,因为这意味着当我们使网络更大(更具表现力)时,我们可以期望更好的性能。 这就是我们将尝试下一步:我们将最后两个隐藏层中的单位数从500增加到1000。我们需要修改这些行:

net7 = NeuralNet(
    # ...
    hidden4_num_units=1000,  # !
    dropout4_p=0.5,
    hidden5_num_units=1000,  # !
    # ...
    )

相比于没有dropout的网络,改进效果更加明显:

 Epoch  | Train loss | Valid loss | Train / Val
--------|--------------|--------------|---------------
    50  | 0.004756 | 0.007043 | 0.675330
   100  | 0.004440 | 0.005321 | 0.834432
   250  | 0.003974 | 0.003928 | 1.011598
   500  | 0.002574 | 0.002347 | 1.096366
  1000  | 0.001861 | 0.001613 | 1.153796
  1500  | 0.001558 | 0.001372 | 1.135849
  2000  | 0.001409 | 0.001230 | 1.144821
  2500  | 0.001295 | 0.001146 | 1.130188
  3000  | 0.001195 | 0.001087 | 1.099271

有一点过拟合,但效果着实不错。我的感觉是,如果继续增加训练次数,模型效果会变得更棒。试一下:

net12 = NeuralNet(
    # ...
    max_epochs=10000,
    # ...
    )
Epoch  | Train loss | Valid loss | Train / Val
--------|--------------|--------------|---------------
    50  | 0.004756 | 0.007027 | 0.676810
   100  | 0.004439 | 0.005321 | 0.834323
   500  | 0.002576 | 0.002346 | 1.097795
  1000  | 0.001863 | 0.001614 | 1.154038
  2000  | 0.001406 | 0.001233 | 1.140188
  3000  | 0.001184 | 0.001074 | 1.102168
  4000  | 0.001068 | 0.000983 | 1.086193
  5000  | 0.000981 | 0.000920 | 1.066288
  6000  | 0.000904 | 0.000884 | 1.021837
  7000  | 0.000851 | 0.000849 | 1.002314
  8000  | 0.000810 | 0.000821 | 0.985769
  9000  | 0.000769 | 0.000803 | 0.957842
 10000  | 0.000760 | 0.000787 | 0.966583

现在你是dropout魔力的见证者了。:-)

让我们比较一下到目前为止我们训练过的网络:

Name Description Epochs Train loss Valid loss
net1 single hidden 400 0.002244 0.003255
net2 convolutions 1000 0.001079 0.001566
net3 augmentation 3000 0.000678 0.001288
net4 mom + lr adj 1000 0.000496 0.001387
net5 net4 + augment 2000 0.000373 0.001184
net6 net5 + dropout 3000 0.001306 0.001121
net7 net6 + epochs 10000 0.000760 0.000787

你可能感兴趣的:(神经网络,Lasagne)