如何为神经网络选择最合适的学习率lr-LRFinder-for-Keras

时间:2024-03-22 22:32:19

keras 版本的LRFinder,借鉴 fast.ai Deep Learning course

前言

学习率lr在神经网络中是最难调的全局参数:设置过大,会导致loss震荡,学习难以收敛;设置过小,那么训练的过程将大大增加。如果,调整一次学习率的周期的训练完一次,那么,训练n次,才能得到n个lr的结果…,导致学习率的选择过程代价太大。。

有多种方法可以为学习速度选择一个好的起点。一个简单的方法是尝试几个不同的值,看看哪个值在不牺牲训练速度的情况下损失最大。我们可以从较大的值开始,比如0.1,然后尝试指数更低的值:0.01、0.001,等等。当我们以较高的学习率开始培训时,损失并没有得到改善,甚至在我们运行前几次迭代培训时可能还会增加。当训练的学习率较小时,在某一点上损失函数的值在前几次迭代中开始减小。这个学习率是我们可以使用的最大值,任何更高的值都不会让训练收敛。甚至这个值也太高了:它还不足以训练多个时代,因为随着时间的推移,网络将需要更细粒度的权重更新。因此,一个合理的开始训练的学习率可能会低1-2个数量级。

学习率的调整困难如下图所示:
如何为神经网络选择最合适的学习率lr-LRFinder-for-Keras
如果,只是在训练的一个epoch中,就可以确定最佳的lr选择,那岂不是美哉?

A smarter way

Leslie N. Smith在2015年发表的论文Cyclical Learning Rates for Training Neural Networks第3.3节中描述了一种选择神经网络学习率范围的强大技术。
诀窍是,从一个较低的学习率开始训练一个网络,并以指数级增长每一批的学习率。
如下图所示:

如何为神经网络选择最合适的学习率lr-LRFinder-for-Keras
记录每个批次的学习率和培训损失。然后,绘制损失和学习率。通常,它看起来是这样的:
如何为神经网络选择最合适的学习率lr-LRFinder-for-Keras
首先,在低学习率的情况下,损失会缓慢地改善,然后训练会加速,直到学习率过大,损失上升:训练过程会出现分歧。

我们需要在图上选择一个损失下降最快的点。在这个例子中,当学习率在0.001到0.01之间时,损失函数下降得很快。

查看这些数字的另一种方法是计算损失的变化率(损失函数对迭代次数的导数),然后在y轴上绘制变化率,在x轴上绘制学习率。
如何为神经网络选择最合适的学习率lr-LRFinder-for-Keras

它看起来太吵了,我们用简单的simple moving average把它弄平。
如何为神经网络选择最合适的学习率lr-LRFinder-for-Keras

这看起来更好。在这个图上,我们需要找到最小值。它接近于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做的实验:
如何为神经网络选择最合适的学习率lr-LRFinder-for-Keras

代码参考:https://github.com/surmenok/keras_lr_finder
博客参考:https://towardsdatascience.com/estimating-optimal-learning-rate-for-a-deep-neural-network-ce32f2556ce0