学习率是深度学习网络中的一个重要超参数——它直接决定了 度 对其执行权重更新,估计这些更新以最小化某些给定的损失函数。 以新元计:
$$
权重_{t+1} = 权重_t – lr * 压裂{derror}{dweight_t}
$$
学习率为 0
,更新后的权重又回到了自身—— 重量t. 学习率实际上是一个旋钮,我们可以转动它来启用或禁用学习,它通过直接控制权重更新的程度,对发生的学习量产生重大影响。
不同的优化器以不同的方式利用学习率——但基本概念保持不变。 不用说,学习率已经成为许多研究、论文和实践者基准的对象。
一般来说,几乎每个人都同意静态学习率不会降低它,并且在大多数在训练期间调整学习率的技术中都会发生某种类型的学习率降低——无论是单调的、余弦的、三角形的还是其他类型的减少。
近几年来已经站稳脚跟的一项技术是 学习率预热,它可以与几乎任何其他减少技术配对。
学习率热身
学习率预热背后的想法很简单。 在训练的最初阶段——权重离他们的理想状态还很远。 这意味着全面的大更新,这可以被视为对每个权重的“过度校正”——另一个权重的剧烈更新可能会否定其他权重的更新,从而使训练的初始阶段更加不稳定。
这些变化可以解决,但可以通过从一开始就采用较小的学习率,达到更稳定的次优状态,然后应用更大的学习率来避免。 您可以将网络简化为更新,而不是使用它们进行更新。
这就是学习率热身! 从低(或 0)学习率开始并增加到起始学习率(无论如何你都会开始)。 这种增加实际上可以遵循任何函数,但通常是线性的。
达到初始速率后,可以应用余弦衰减、线性缩减等其他方案来逐步降低速率,直到训练结束。 学习率预热通常是两个计划的一部分,其中 LR 预热是第一个,而另一个计划在速率达到起点后接管。
在本指南中,我们将在 Keras/TensorFlow 中实现学习率预热作为 keras.optimizers.schedules.LearningRateSchedule
子类和 keras.callbacks.Callback
打回来。 学习率将从 0
至 target_lr
并应用余弦衰减,因为这是一个非常常见的二级时间表。 像往常一样,Keras 使以各种方式实施灵活的解决方案并将它们与您的网络一起发布变得简单。
请注意: 实现是通用的,灵感来自 Tony 的 Keras 实现 在“使用卷积神经网络进行图像分类的技巧包”.
使用 Keras 回调的学习率
实现任何学习率计划的最简单方法是创建一个函数,该函数采用 lr
参数(float32
),通过一些转换传递它,然后返回它。 然后将该函数传递给 LearningRateScheduler
回调,它将函数应用于学习率。
现在, tf.keras.callbacks.LearningRateScheduler()
将 epoch 数传递给它用来计算学习率的函数,这非常粗糙。 LR 预热应该在每个 步 (batch),而不是 epoch,所以我们必须推导出一个 global_step
(跨越所有时期)来计算学习率,并将 Callback
类来创建自定义回调,而不仅仅是传递函数,因为我们需要在每次调用时传递参数,这在传递函数时是不可能的:
def func():
return ...
keras.callbacks.LearningRateScheduler(func)
当您不想要高级定制并且不想干扰 Keras 对待 lr
, 特别是如果你想使用回调 ReduceLROnPlateau()
因为它只能与基于浮点的 lr
. 让我们使用 Keras 回调实现学习率预热,从一个便利函数开始:
def lr_warmup_cosine_decay(global_step,
warmup_steps,
hold = 0,
total_steps=0,
start_lr=0.0,
target_lr=1e-3):
learning_rate = 0.5 * target_lr * (1 + np.cos(np.pi * (global_step - warmup_steps - hold) / float(total_steps - warmup_steps - hold)))
warmup_lr = target_lr * (global_step / warmup_steps)
if hold > 0:
learning_rate = np.where(global_step > warmup_steps + hold,
learning_rate, target_lr)
learning_rate = np.where(global_step < warmup_steps, warmup_lr, learning_rate)
return learning_rate
在每一步,我们计算学习率和预热学习率(计划的两个元素),关于 start_lr
和 target_lr
. start_lr
通常会从 0.0
,而 target_lr
取决于您的网络和优化器 – 1e-3
可能不是一个好的默认值,所以在调用方法时一定要设置你的目标开始 LR。
如果 global_step
在训练中高于 warmup_steps
我们已经设置——我们使用余弦衰减时间表 LR。 如果不是,说明我们还在热身,所以使用了warmup LR。 如果 hold
参数已设置,我们将持有 target_lr
对于预热之后和余弦衰减之前的步数。 np.where()
为此提供了一个很好的语法:
np.where(condition, value_if_true, value_if_false)
您可以使用以下方法可视化该功能:
steps = np.arange(0, 1000, 1)
lrs = []
for step in steps:
lrs.append(lr_warmup_cosine_decay(step, total_steps=len(steps), warmup_steps=100, hold=10))
plt.plot(lrs)
现在,我们希望将此函数用作回调的一部分,并将优化器步骤作为 global_step
而不是任意数组的元素——或者您可以在类中执行计算。 让我们子类化 Callback
类:
from keras import backend as K
class WarmupCosineDecay(keras.callbacks.Callback):
def __init__(self, total_steps=0, warmup_steps=0, start_lr=0.0, target_lr=1e-3, hold=0):
super(WarmupCosineDecay, self).__init__()
self.start_lr = start_lr
self.hold = hold
self.total_steps = total_steps
self.global_step = 0
self.target_lr = target_lr
self.warmup_steps = warmup_steps
self.lrs = []
def on_batch_end(self, batch, logs=None):
self.global_step = self.global_step + 1
lr = model.optimizer.lr.numpy()
self.lrs.append(lr)
def on_batch_begin(self, batch, logs=None):
lr = lr_warmup_cosine_decay(global_step=self.global_step,
total_steps=self.total_steps,
warmup_steps=self.warmup_steps,
start_lr=self.start_lr,
target_lr=self.target_lr,
hold=self.hold)
K.set_value(self.model.optimizer.lr, lr)
查看我们的 Git 学习实践指南,其中包含最佳实践、行业认可的标准以及随附的备忘单。 停止谷歌搜索 Git 命令,实际上 学习 它!
首先,我们为类定义构造函数并跟踪其字段。 在每个批次结束时,我们将增加全局步长,记下当前的 LR 并将其添加到目前为止的 LR 列表中。 在每个批次的开始——我们将使用 lr_warmup_cosine_decay()
函数并将该 LR 设置为优化器的当前 LR。 这是通过后端的 set_value()
.
完成后 - 只需计算总步数(长度/batch_size*epochs)并为您的 warmup_steps
:
total_steps = len(train_set)*config['EPOCHS']
warmup_steps = int(0.05*total_steps)
callback = WarmupCosineDecay(total_steps=total_steps,
warmup_steps=warmup_steps,
hold=int(warmup_steps/2),
start_lr=0.0,
target_lr=1e-3)
最后,构建您的模型并在 fit()
拨打:
model = keras.applications.EfficientNetV2B0(weights=None,
classes=n_classes,
input_shape=[224, 224, 3])
model.compile(loss="sparse_categorical_crossentropy",
optimizer='adam',
jit_compile=True,
metrics=['accuracy'])
在训练结束时,您可以通过以下方式获取并可视化更改的 LR:
lrs = callback.lrs
plt.plot(lrs)
如果您绘制使用和不使用 LR 预热训练的模型的历史 - 您会看到训练稳定性的明显差异:
LearningRateSchedule 子类的学习率
创建回调的另一种方法是创建一个 LearningRateSchedule
子类,它不操纵 LR - 它替换它。 这种方法允许您对 Keras/TensorFlow 的后端进行更多的探索,但是在使用时,不能与其他与 LR 相关的回调结合使用,例如 ReduceLROnPlateau()
,它将 LR 处理为浮点数。
此外,使用子类将要求您使其可序列化(重载 get_config()
) 因为它成为模型的一部分,如果您想保存模型权重。 另一件需要注意的事情是,该课程将期望专门与 tf.Tensor
s。 值得庆幸的是,我们工作方式的唯一区别就是打电话 tf.func()
而不是 np.func()
因为 TensorFlow 和 NumPy API 惊人地相似且兼容。
让我们重写方便 lr_warmup_cosine_decay()
函数改为使用 TensorFlow 操作:
def lr_warmup_cosine_decay(global_step,
warmup_steps,
hold = 0,
total_steps=0,
start_lr=0.0,
target_lr=1e-3):
learning_rate = 0.5 * target_lr * (1 + tf.cos(tf.constant(np.pi) * (global_step - warmup_steps - hold) / float(total_steps - warmup_steps - hold)))
warmup_lr = target_lr * (global_step / warmup_steps)
if hold > 0:
learning_rate = tf.where(global_step > warmup_steps + hold,
learning_rate, target_lr)
learning_rate = tf.where(global_step < warmup_steps, warmup_lr, learning_rate)
return learning_rate
使用 convinience 函数,我们可以子类化 LearningRateSchedule
班级。 在各个 __call__()
(batch),我们将使用函数计算 LR 并将其返回。 您也可以自然地将计算打包在子类中。
语法比 Callback
sublcas,主要是因为我们可以访问 step
字段,而不是我们自己跟踪它,但也使得使用类属性变得有些困难——特别是,它使得提取 lr
从 tf.Tensor()
进入任何其他类型以在列表中跟踪。 这可以通过在急切模式下运行在技术上规避,但会为跟踪 LR 以进行调试带来麻烦,最好避免:
class WarmUpCosineDecay(keras.optimizers.schedules.LearningRateSchedule):
def __init__(self, start_lr, target_lr, warmup_steps, total_steps, hold):
super().__init__()
self.start_lr = start_lr
self.target_lr = target_lr
self.warmup_steps = warmup_steps
self.total_steps = total_steps
self.hold = hold
def __call__(self, step):
lr = lr_warmup_cosine_decay(global_step=step,
total_steps=self.total_steps,
warmup_steps=self.warmup_steps,
start_lr=self.start_lr,
target_lr=self.target_lr,
hold=self.hold)
return tf.where(
step > self.total_steps, 0.0, lr, name="learning_rate"
)
参数是相同的,并且可以以与以前大致相同的方式计算:
total_steps = len(train_set)*config['EPOCHS']
warmup_steps = int(0.05*total_steps)
schedule = WarmUpCosineDecay(start_lr=0.0, target_lr=1e-3, warmup_steps=warmup_steps, total_steps=total_steps, hold=warmup_steps)
训练管道的不同之处仅在于我们将优化器的 LR 设置为 schedule
:
model = keras.applications.EfficientNetV2B0(weights=None,
classes=n_classes,
input_shape=[224, 224, 3])
model.compile(loss="sparse_categorical_crossentropy",
optimizer=tf.keras.optimizers.Adam(learning_rate=schedule),
jit_compile=True,
metrics=['accuracy'])
history3 = model.fit(train_set,
epochs = config['EPOCHS'],
validation_data=valid_set)
如果你想保存模型, WarmupCosineDecay
时间表将不得不覆盖 get_config()
方法:
def get_config(self):
config = {
'start_lr': self.start_lr,
'target_lr': self.target_lr,
'warmup_steps': self.warmup_steps,
'total_steps': self.total_steps,
'hold': self.hold
}
return config
最后,在加载模型时,您必须通过 WarmupCosineDecay
作为自定义对象:
model = keras.models.load_model('weights.h5',
custom_objects={'WarmupCosineDecay', WarmupCosineDecay})
结论
在本指南中,我们了解了学习率预热背后的直觉——这是一种在训练神经网络时操纵学习率的常用技术。
我们已经使用余弦衰减实现了学习率预热,这是最常见的 LR 减少类型与预热配对。 您可以实现任何其他函数来减少,或者根本不降低学习率——将其留给其他回调,例如 ReduceLROnPlateau()
. 我们已经将学习率预热实现为 Keras 回调,以及 Keras 优化器计划,并绘制了各个时期的学习率。