第七部分 让 学习率 和 学习势 随着时间改变
上个模型令人讨厌的地方是光训练就花了一个小时的时间,等结果并不是一个令人心情愉快的事情。这一部分,我们将讨论将两个技巧结合让网络训练的更快!
直觉上的解决办法是,开始训练时取一个较高的学习率,随着迭代次数的增多不停的减小这个值。这是有道理的,因为开始的时候我们距离全局最优点非常远,我们想要朝着最优点的方向大步前进;然而里最优点越近,我们就前进的越谨慎,以免一步跨过去。举个例子说就是你乘火车回家,但你进家门的时候肯定是走进去,不能让火车开进去。
从讨论深度学习中初始化和学习势的重要性的资料,我们得到了一种新的技巧来加速网络的训练:增加最优化方法的“动量参数”。如果你还不知道什么是学习势,请阅读【参考】。
在我们上一个模型中,我们将学习率和学习势初始化为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次迭代的训练错误,net4都比net2低了一半。但是泛华程度到750次就不再变好了,所以看起来没有必要训练这么多次。
看看打开了数据扩充之后的net5表现如何:
Epoch | 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相比net5训练的快多了,并且获得了更好的结果。在1000次迭代后,结果比net3迭代了3000次的效果还要好。此外,使用了数据扩充的网络的验证错误也比不使用数据扩充好了10%。
第八部分 丢弃技巧(Dropout)
2012年,这篇paper中引入了一种叫做“Dropout”的技巧,作为正则化方法,dropout工作的出奇的好。这里不会讲dropout之所以好用的细节,不过你可以阅读这些参考。
和其他的正则化技巧一样,dropout只有当网络过拟合的时候才有意义。上个部分我们训练的net5是明显过拟合的。注意一定要使你的网络过拟合,再使用正则化。
在Lasagne中使用正则化技巧,我们只要在网络中添加DropoutLayer并指定每层dropout的概率。这里是我们最新网络的完整定义,我在新添加的行后面添加了#!来标识与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 |
仍然过拟合不一定是坏事,尽管我们必须小心这些数字:训练错误和验证错误的比率现在的意义稍有不同,因为训练错误是受过dropout调制的,而验证错误没有。更有比较意义的数值是:
from sklearn.metrics import mean_squared_error
print mean_squared_error(net6.predict(X), y)
# prints something like 0.0010073791
在我们上一个没有dropout的模型里,训练集上的错误是0.00373。所以现在我们的dropout net不但表现稍好,而且过拟合水平更低。这是好消息,因为我们可以通过使得网络更大获得更好的效果。这也正是我们接下来要做的,我们加倍网络最后两个隐层的单元个数。更新这两行:
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 |
使用CNN(convolutional neural nets)检测脸部关键点教程(一):环境搭建和数据
使用CNN(convolutional neural nets)检测脸部关键点教程(二):浅层网络训练和测试
使用CNN(convolutional neural nets)检测脸部关键点教程(三):卷积神经网络训练和数据扩充
使用CNN(convolutional neural nets)检测脸部关键点教程(五):通过前训练(pre-train)训练专项网络