学习率规划
找到一个合适的学习率非常重要。如果将学习率设置过高,模型训练可能会发散。如果设置过低,那么训练虽然会收敛至最优,但是会耗费大量的时间。如果你将学习率设置得稍微有点高,那么模型在一开始训练收敛的速度会很快,但是最终会在最优点附近徘徊,无法真正收敛至最优。如果你拥有的计算资源有限,那么你可能会在收敛前就中止训练,生成一个次优解(见图2.8)。
在之前的文章中就提到过,一个寻找合适的学习率的策略是,对模型进行几百次迭代训练,并且从一个很小的学习率开始成倍提高。然后根据学习曲线,如果有一个学习曲线开始向上发散,那么就挑选比发散学习率稍小的即可。然后再重新初始化模型,用选中的学习率进行训练。
但是我们的模型可以比恒定学习率做得更好:如果你从一个很大的学习率开始,然后在训练无法取得进展时降低它,你就可以比最优恒定学习率更快地得到一个好的结果。这里有很多不同的在训练中降低学习率的方法。有些甚至会从一个小的学习率开始,不断调高,然后再减小。这些策略被称为学习率的规划(learning schedules)。接下来我们来看一些常用的学习率策略:
- 幂规划
将学习率设置为循环次数t的函数,如下式:
初始学习率η0,幂c(通常设置为1),步数s都是超参数。学习率会在每一步训练都下降。在s步后,学习率下降为η0/2;2s步后,下降为η0/3,依次类推。可以看到,这个规划方法一开始学习率下降很快,之后会越来越慢,并且,上述三个超参数都是需要调整的。
- 指数规划
将学习率设置为:
那么每过s步学习率就会逐渐降低至原来的十分之一。上面提到的幂规划法减小学习率的速度会逐渐降低,而指数规划方法每过s步就变成0.1倍。
- 分段常数规划
在几个epoch以内的训练中,使用一个常数作为学习率(比如在5个epoch中η0 = 0.1),然后下几个epoch使用另一个更小的学习率(比如在下50个epoch当中η1 = 0.001),如此进行。这个方法的表现可以很出色,但是它需要大量的尝试来找出正确的学习率序列,以及每个学习率的最佳使用时长。
- 性能规划
在每N步评估一次验证误差(就像早停方法),当发现验证误差不再下降时,就将学习率降低λ分之一。
- 单循环规划
Leslie Smith2018年发表的论文中介绍了单循环法。与其他的方法不同,单循环法首先从提高初始学习率η0开始,随着训练的进行在训练至一半时线性增长至η1。然后在训练的后半段再次线性下降至η0,在训练的最后几个epoch,学习率会线性减小几个数量级。其中最大学习率η1的选择方法与上述的最佳学习率选择方法相同,在选好了最大学习率之后只需将初始学习率η0设置为最大的十分之一即可。
当配合使用动量优化时,也首先需要设定一个较高的动量参数(比如0.95),随后在训练的前半段线性降低至一个较低的动量值(比如0.85),之后再训练的后半段再提升至最大动量值,最后一直保持这个最大值直至训练结束。
作者Smith的实验结果表明,这种方法通常能够大大加快训练速度,提高模型性能。比如在CIFAR10图形数据集上,这种方法仅在100个epoch内就达到了91.9%的验证精度,而用普通方法需要800个epoch才达到90.3%的精度。
Andrew Senior等人在2013年的一篇论文中比较了一些最流行了学习率规划方法与动量优化配合训练出用于语音识别的深度神经网络。作者的结论是,在这种情况下,性能规划和指数规划的表现比较出色。他们更倾向于指数规划法,因为它易于调优,并且收敛速度也稍快。他们还提到,指数规划比性能规划更容易实现,不过在Keras当中,这两个都很容易。作者还指出,使用单循环规划表现得更为出色。
在Keras中实现幂规划非常简单,只要在创建优化器时设置好decay
超参数:
optimizer = keras.optimizers.SGD(lr=0.01, decay=1e-4)
decay
超参数是参数s的倒数,并且Keras假设c等于1。
指数规划和分段规划的部署也很简单。首先要定义一个接收当前epoch并返回学习速率的函数,我们来拿一个指数规划做例子:
def exponential_decay_fn(epoch):
return 0.01 * 0.1**(epoch / 20)
如果你不想硬编码η0和s,你可以创建一个返回配置函数的函数:
def exponential_decay(lr0, s):
def exponential_decay_fn(epoch):
return lr0 * 0.1**(epoch / s)
return exponential_decay_fn
exponential_decay_fn = exponential_decay(lr0=0.01, s=20)
接着创建一个LearningRateScheduler
回调函数,输入一个规划函数,然后将这个回调函数输入给fit()
方法:
lr_scheduler = keras.callbacks.LearningRateScheduler(exponential_decay_fn)
history = model.fit(X_train_scaled, y_train, [...], callbacks=[lr_scheduler])
LearningRateScheduler
会在每个epoch开始的时候将优化器的learning_rate属性更新一次。一般来说每个epoch更新一次学习率就足够了,但是如果想要更加频繁地更新学习率,比如在每一步都更改学习率,那么可以写自己的回调函数。实际上如果每个epoch当中有很多步的话,对于每一步都更新学习率是有意义的。或者,可以使用keras.optimizers.schedules
方法,在稍后会介绍这种方法。
规划函数还可以将当前学习率作为第二个参数。比如,下面这个规划函数就将当前学习率乘以0.11/20,这样也能够形成指数衰减(不同的是这个衰减从第0个epoch就开始了,而不是第1个)。
def exponential_decay_fn(epoch, lr):
return lr * 0.1**(1 / 20)
这个实现方法比较依赖优化器的初始学习率,所以需要确保设置一个合适的初始值。
当保存一个模型时,优化器和学习率也会一起被保存。也就是说,有了上面这个规划函数,你可以加载一个已经训练过的模型,然后在它停止训练的地方继续训练,不会有任何问题。不过如果规划函数使用了epoch参数,那就不那么简单了:因为epoch不会被保存下来,并且每次调用fit()
都会将epoch重新设为0。那么如果要在模型停止处继续训练,那么学习率可能会过大,那么可能会影响到模型权重。其中一种解决办法就是手动设置fit()
方法中的initial_epoch
参数,使得epoch从你想要的数字开始。
对于分段常数规划,你可以使用一个类似下面的规划函数(你甚至可以定义一个更为通用的函数),然后创建一个LearningRateScheduler
回调函数,并将其输入fit()
方法,跟前面的方法一样:
def piecewise_constant_fn(epoch):
if epoch < 5:
return 0.01
elif epoch < 15:
return 0.005
else:
return 0.001
对于性能规划方法,使用ReduceLROnPlateau
回调函数即可。比如,将下述回调函数传递给fit()
方法,它会在连续5个验证损失不下降的epoch后,将学习率乘以0.5。
lr_scheduler = keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5)
最后,tf.keras提供了一种学习率规划的替代方法:使用keras.optimizers.schedules
中可用的规划来定义学习率,然后将学习率输入任意的优化器。这个方法会在每一步都更新学习率而不是每一个epoch。这里展示一下如何实现与上文相同的指数规划:
s = 20 * len(X_train) // 32 # number of steps in 20 epochs (batch size = 32)
learning_rate = keras.optimizers.schedules.ExponentialDecay(0.01, s, 0.1)
optimizer = keras.optimizers.SGD(learning_rate)
这就非常方便了,并且当我们保存模型时,学习率和它的规划(以及规划的状态)都会被保存下来。不过这个方法,并不是Keras的API,而只专属于tf.keras。
而单循环方法,目前并没有直接可用的方法。不过实现起来也并不困难,需要创建一个定制的回调,在每次迭代中修改学习率即可(可以通过修改self.model.optimizer.lr
来更新优化器的学习率)。
总而言之,指数衰减,性能规划和单循环规划都可以显著加快收敛速度。所以推荐尝试一下。
下一回我们将来讲述如何防止过拟合。
敬请期待啦!