keras 版本的LRFinder,借鉴 fast.ai Deep Learning course。
前言
学习率lr在神经网络中是最难调的全局参数:设置过大,会导致loss震荡,学习难以收敛;设置过小,那么训练的过程将大大增加。如果,调整一次学习率的周期的训练完一次,那么,训练n次,才能得到n个lr的结果…,导致学习率的选择过程代价太大。。
有多种方法可以为学习速度选择一个好的起点。一个简单的方法是尝试几个不同的值,看看哪个值在不牺牲训练速度的情况下损失最大。我们可以从较大的值开始,比如0.1,然后尝试指数更低的值:0.01、0.001,等等。当我们以较高的学习率开始培训时,损失并没有得到改善,甚至在我们运行前几次迭代培训时可能还会增加。当训练的学习率较小时,在某一点上损失函数的值在前几次迭代中开始减小。这个学习率是我们可以使用的最大值,任何更高的值都不会让训练收敛。甚至这个值也太高了:它还不足以训练多个时代,因为随着时间的推移,网络将需要更细粒度的权重更新。因此,一个合理的开始训练的学习率可能会低1-2个数量级。
学习率的调整困难如下图所示:
如果,只是在训练的一个epoch中,就可以确定最佳的lr选择,那岂不是美哉?
A smarter way
Leslie N. Smith在2015年发表的论文Cyclical Learning Rates for Training Neural Networks第3.3节中描述了一种选择神经网络学习率范围的强大技术。
诀窍是,从一个较低的学习率开始训练一个网络,并以指数级增长每一批的学习率。
如下图所示:
记录每个批次的学习率和培训损失。然后,绘制损失和学习率。通常,它看起来是这样的:
首先,在低学习率的情况下,损失会缓慢地改善,然后训练会加速,直到学习率过大,损失上升:训练过程会出现分歧。
我们需要在图上选择一个损失下降最快的点。在这个例子中,当学习率在0.001到0.01之间时,损失函数下降得很快。
查看这些数字的另一种方法是计算损失的变化率(损失函数对迭代次数的导数),然后在y轴上绘制变化率,在x轴上绘制学习率。
它看起来太吵了,我们用简单的simple moving average把它弄平。
这看起来更好。在这个图上,我们需要找到最小值。它接近于lr=0.01。
keras 实现
from matplotlib import pyplot as plt
import math
from keras.callbacks import LambdaCallback
import keras.backend as K
class LRFinder:
"""
Plots the change of the loss function of a Keras model when the learning rate is exponentially increasing.
See for details:
https://towardsdatascience.com/estimating-optimal-learning-rate-for-a-deep-neural-network-ce32f2556ce0
"""
def __init__(self, model):
self.model = model
self.losses = []
self.lrs = []
self.best_loss = 1e9
def on_batch_end(self, batch, logs):
# Log the learning rate
lr = K.get_value(self.model.optimizer.lr)
self.lrs.append(lr)
# Log the loss
loss = logs['loss']
self.losses.append(loss)
# Check whether the loss got too large or NaN
if math.isnan(loss) or loss > self.best_loss * 4:
self.model.stop_training = True
return
if loss < self.best_loss:
self.best_loss = loss
# Increase the learning rate for the next batch
lr *= self.lr_mult
K.set_value(self.model.optimizer.lr, lr)
def find(self, x_train, y_train, start_lr, end_lr, batch_size=64, epochs=1):
num_batches = epochs * x_train.shape[0] / batch_size
self.lr_mult = (float(end_lr) / float(start_lr)) ** (float(1) / float(num_batches))
# Save weights into a file
self.model.save_weights('tmp.h5')
# Remember the original learning rate
original_lr = K.get_value(self.model.optimizer.lr)
# Set the initial learning rate
K.set_value(self.model.optimizer.lr, start_lr)
callback = LambdaCallback(on_batch_end=lambda batch, logs: self.on_batch_end(batch, logs))
self.model.fit(x_train, y_train,
batch_size=batch_size, epochs=epochs,
callbacks=[callback])
# Restore the weights to the state before model fitting
self.model.load_weights('tmp.h5')
# Restore the original learning rate
K.set_value(self.model.optimizer.lr, original_lr)
def plot_loss(self, n_skip_beginning=10, n_skip_end=5):
"""
Plots the loss.
Parameters:
n_skip_beginning - number of batches to skip on the left.
n_skip_end - number of batches to skip on the right.
"""
plt.ylabel("loss")
plt.xlabel("learning rate (log scale)")
plt.plot(self.lrs[n_skip_beginning:-n_skip_end], self.losses[n_skip_beginning:-n_skip_end])
plt.xscale('log')
def plot_loss_change(self, sma=1, n_skip_beginning=10, n_skip_end=5, y_lim=(-0.01, 0.01)):
"""
Plots rate of change of the loss function.
Parameters:
sma - number of batches for simple moving average to smooth out the curve.
n_skip_beginning - number of batches to skip on the left.
n_skip_end - number of batches to skip on the right.
y_lim - limits for the y axis.
"""
assert sma >= 1
derivatives = [0] * sma
for i in range(sma, len(self.lrs)):
derivative = (self.losses[i] - self.losses[i - sma]) / sma
derivatives.append(derivative)
plt.ylabel("rate of loss change")
plt.xlabel("learning rate (log scale)")
plt.plot(self.lrs[n_skip_beginning:-n_skip_end], derivatives[n_skip_beginning:-n_skip_end])
plt.xscale('log')
plt.ylim(y_lim)
可以修改find函数,来适应fit_generator。
def find(self, aug_gen, start_lr, end_lr, batch_size=600, epochs=1, num_train = 10000):
num_batches = epochs * num_train / batch_size
steps_per_epoch = num_train / batch_size
self.lr_mult = (float(end_lr) / float(start_lr)) ** (float(1) / float(num_batches))
# Save weights into a file
self.model.save_weights('tmp.h5')
# Remember the original learning rate
original_lr = K.get_value(self.model.optimizer.lr)
# Set the initial learning rate
K.set_value(self.model.optimizer.lr, start_lr)
callback = LambdaCallback(on_batch_end=lambda batch, logs: self.on_batch_end(batch, logs))
self.model.fit_generator(aug_gen,
epochs=epochs,
steps_per_epoch=steps_per_epoch,
callbacks=[callback])
# Restore the weights to the state before model fitting
self.model.load_weights('tmp.h5')
# Restore the original learning rate
K.set_value(self.model.optimizer.lr, original_lr)
代码解析
代码主要是使用了keras中的回调函数,LambdaCallback
函数详情:
keras.callbacks.LambdaCallback(on_epoch_begin=None, on_epoch_end=None, on_batch_begin=None, on_batch_end=None, on_train_begin=None, on_train_end=None)
在训练进行中创建简单,自定义的回调函数的回调函数。
这个回调函数和匿名函数在合适的时间被创建。 需要注意的是回调函数要求位置型参数,如下:
on_epoch_begin 和 on_epoch_end 要求两个位置型的参数: epoch, logs
on_batch_begin 和 on_batch_end 要求两个位置型的参数: batch, logs
on_train_begin 和 on_train_end 要求一个位置型的参数: logs
参数
on_epoch_begin: 在每轮开始时被调用。
on_epoch_end: 在每轮结束时被调用。
on_batch_begin: 在每批开始时被调用。
on_batch_end: 在每批结束时被调用。
on_train_begin: 在模型训练开始时被调用。
on_train_end: 在模型训练结束时被调用。
例子:
# 在每一个批开始时,打印出批数。
batch_print_callback = LambdaCallback(
on_batch_begin=lambda batch,logs: print(batch))
下面是我在 kaggle Histopathologic Cancer Detection做的实验:
代码参考:https://github.com/surmenok/keras_lr_finder
博客参考:https://towardsdatascience.com/estimating-optimal-learning-rate-for-a-deep-neural-network-ce32f2556ce0