學習率調度

原文鏈接:https://d2l.ai/chapter_optimization/lr-scheduler.html

在神經網絡中,通常我們主要關注優化算法如何更新權重,而缺少關注更新的幅度,即學習率。適當的調整學習率和優化算法一樣重要。可以從這些角度去考慮:

  • 【學習率大小】最直觀的就是學習率的粒度很重要。如果學習率太大,優化曲線就會發散,如果學習率太小,訓練時間會超級長,或者最終得到一個次優結果。
  • 【衰減因子】其次,衰減因子也很重要,如果學習率一直保持較大的值,那麼最終可能會在最優點附近反覆跳動,而無法得到最優解。
  • 【預熱】另一個重要的方面就是初始化。包括參數如何初始化以及參數初期如何進化。這被稱爲預熱warmup,也就是在訓練初始的時候,我們向解決方案前進的速度有多快。開始的時候大步前進可能沒有好處,因爲初始設置的參數是隨機設置的,所以初始的更新方向可能是沒有意義的。
  • 最後,有很多優化變量都在進行週期性的學習率調整。因此,推薦閱讀“Averaging weights leads to wider optima and better generalization”,如何通過在整個參數路徑上求平均值來獲得更好的解。

基於學習率調整有很多細節,所以很多深度學習框架都有工具來自動處理這件事。

1. 舉個例子

下邊是使用類似LeNet的結構對Fashion-MNIST數據(一個類似手寫體數字識別的圖像分類數據集)進行分類。

from d2l import tensorflow as d2l
import tensorflow as tf
import math
from tensorflow.keras.callbacks import LearningRateScheduler

def net():
    return tf.keras.models.Sequential([
        tf.keras.layers.Conv2D(filters=6, kernel_size=5, activation='relu',
                               padding='same'),
        tf.keras.layers.AvgPool2D(pool_size=2, strides=2),
        tf.keras.layers.Conv2D(filters=16, kernel_size=5,
                               activation='relu'),
        tf.keras.layers.AvgPool2D(pool_size=2, strides=2),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(120, activation='relu'),
        tf.keras.layers.Dense(84, activation='sigmoid'),
        tf.keras.layers.Dense(10)])


batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

# The code is almost identical to `d2l.train_ch6` defined in the
# lenet section of chapter convolutional neural networks
def train(net_fn, train_iter, test_iter, num_epochs, lr,
              device=d2l.try_gpu(), custom_callback = False):
    device_name = device._device_name
    strategy = tf.distribute.OneDeviceStrategy(device_name)
    with strategy.scope():
        optimizer = tf.keras.optimizers.SGD(learning_rate=lr)
        loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
        net = net_fn()
        net.compile(optimizer=optimizer, loss=loss, metrics=['accuracy'])
    callback = d2l.TrainCallback(net, train_iter, test_iter, num_epochs,
                             device_name)
    if custom_callback is False:
        net.fit(train_iter, epochs=num_epochs, verbose=0,
                callbacks=[callback])
    else:
         net.fit(train_iter, epochs=num_epochs, verbose=0,
                 callbacks=[callback, custom_callback])
    return net

讓我們看一下默認設置情況下,算法的學習情況,比如lr=0.3,iterations=30. 可以注意到,當測試準確率在某一點後就停滯不前了,但是訓練準確率還在一直提升。這兩個曲線之間的間隙表示模型過擬合了。

lr, num_epochs = 0.3, 30
train(net, train_iter, test_iter, num_epochs, lr)

2. 調度器

一種調整學習率的方法就是每一個step都明確指定learning rate。這個可以通過set_learning_rate方法做到。我們可以再每個epoch或mini-batch之後調小一點。也就是根據優化的進度進行動態調整。

lr = 0.1
dummy_model = tf.keras.models.Sequential([tf.keras.layers.Dense(10)])
dummy_model.compile(tf.keras.optimizers.SGD(learning_rate=lr), loss='mse')
print(f'learning rate is now ,', dummy_model.optimizer.lr.numpy())

更通用的方式是希望定一個調度器。當傳入更新的次數,它能返回一個適當的學習率。比如我們可以定一個和t的平方根相關的調度器:

class SquareRootScheduler:
    def __init__(self, lr=0.1):
        self.lr = lr

    def __call__(self, num_update):
        return self.lr * pow(num_update + 1.0, -0.5)

我們來看一下SquareRootScheduler是如何隨着epoch變化的。

scheduler = SquareRootScheduler(lr=0.1)
d2l.plot(tf.range(num_epochs), [scheduler(t) for t in range(num_epochs)])

下邊讓我們看下這個調度器在FashionMNIST數據集上的表現。

train(net, train_iter, test_iter, num_epochs, lr,
      custom_callback=LearningRateScheduler(scheduler))

從上圖可以看到,這個調度器已經比前邊的表現好一些了。可以看出兩個現象:

  • 和前邊的模型相比,這次曲線更加平滑;
  • 這次的過擬合得到了緩解;
    不過,這個事實無法很好的回答,爲什麼某些策略會緩解過擬合。有些觀點認爲更小的步長導致參數更新幅度接近於0,但是這無法很好的完全解釋上述現象,因爲我們並沒有真的停止,而僅僅是降低了學習率。

常用的調度策略

常規選擇是多項式衰減和分段常數調度。此外,cosine學習率調度也被證明在某些任務上表現不錯。最後,有些工作適合在使用大的學習率之前,先預熱一下。

1. 因子調度器

一種多項式衰減策略就是乘上alpha因子,lr_t+1 = lr_t * alpha, 0 < alpha < 1。爲了避免學習率過度衰減,超多一個合理的下界,這個等式通常寫作:lr_t+1 = max( lr_t * alpha, lr_min )

class FactorScheduler:
    def __init__(self, factor=1, stop_factor_lr=1e-7, base_lr=0.1):
        self.factor = factor
        self.stop_factor_lr = stop_factor_lr
        self.base_lr = base_lr

    def __call__(self, num_update):
        self.base_lr = max(self.stop_factor_lr, self.base_lr * self.factor)
        return self.base_lr

scheduler = FactorScheduler(factor=0.9, stop_factor_lr=1e-2, base_lr=2.0)
d2l.plot(tf.range(50), [scheduler(t) for t in range(50)])

2. 多因子調度器

一個常規的策略是,在訓練深度網絡時,分階段保持學習率爲一個常量,然後每個階段都調小一些。也就是說,給定一個時間集合,表示什麼時候調小學習率,比如{5,10,20},也就是當step處於這個集合時,才衰減。比如下邊的實現是每次衰減一半。

class MultiFactorScheduler:
    def __init__(self, step, factor, base_lr):
        self.step = step
        self.factor = factor
        self.base_lr = base_lr

    def __call__(self, epoch):
        if epoch in range(self.step[0], (self.step[1] + 1)):
            return self.base_lr * self.factor
        else:
            return self.base_lr

scheduler = MultiFactorScheduler(step=[15, 30], factor=0.5, base_lr=0.5)
d2l.plot(tf.range(num_epochs), [scheduler(t) for t in range(num_epochs)])

分階段常數學習率的直覺解釋是,每個學習率都可以讓優化一直進行到一個權重向量分佈比較穩定的狀態。然後我們降低學習率,可以得到一個更優的局部最小值。

train(net, train_iter, test_iter, num_epochs, lr,
      custom_callback=LearningRateScheduler(scheduler))

3. Cosine調度器

有人提出了一個相當令人費解的啓發式調度方法。它基於這樣的觀察,我們可能不想在一開始就大幅度的降低學習率,同時,我們又想在最後的時候使用非常小的學習率來改進結果。這就產生了一個類似於餘弦的時間表,其學習率的函數形式如下

n_0是初始學習率,n_T是T時刻學習率,對於t>T的時候,我們固定學習率爲n_T,不再增加。下邊的例子中,T=20.

class CosineScheduler:
    def __init__(self, max_update, base_lr=0.01, final_lr=0,
               warmup_steps=0, warmup_begin_lr=0):
        self.base_lr_orig = base_lr
        self.max_update = max_update
        self.final_lr = final_lr
        self.warmup_steps = warmup_steps
        self.warmup_begin_lr = warmup_begin_lr
        self.max_steps = self.max_update - self.warmup_steps

    def get_warmup_lr(self, epoch):
        increase = (self.base_lr_orig - self.warmup_begin_lr) \
                       * float(epoch) / float(self.warmup_steps)
        return self.warmup_begin_lr + increase

    def __call__(self, epoch):
        if epoch < self.warmup_steps:
            return self.get_warmup_lr(epoch)
        if epoch <= self.max_update:
            self.base_lr = self.final_lr + (self.base_lr_orig - self.final_lr) * \
                    (1 + math.cos(math.pi * (epoch - self.warmup_steps) /
                                  self.max_steps)) / 2
        return self.base_lr


scheduler = CosineScheduler(max_update=20, base_lr=0.3, final_lr=0.01)
d2l.plot(tf.range(num_epochs), [scheduler(t) for t in range(num_epochs)])


在CV領域,這個調度策略可以帶來提升。但是,這個提升是無法被保障的。

train(net, train_iter, test_iter, num_epochs, lr,
      custom_callback=LearningRateScheduler(scheduler))

4. Warmup

在某些情況下,初始化參數不足以保證一個好的解決方案。這對於一些高級網絡設計來說尤其是一個問題,它可能導致不穩定的優化。我們可以通過選擇足夠小的學習率來解決這個問題,以防止在開始時出現發散。不幸的是,這意味着訓練會很緩慢。相反,一個大的學習率最初會導致發散。解決這一困境的一個相當簡單的方法是增加預熱階段,在預熱階段會將學習率提高到初始最大值。然後進入冷卻階段,直到優化過程結束。爲了簡單起見,通常使用線性增長來實現這一目的。就像如下展示的那樣:

scheduler = CosineScheduler(20, warmup_steps=5, base_lr=0.3, final_lr=0.01)   # 前5步進行預熱,直到base lr=0.3
d2l.plot(tf.range(num_epochs), [scheduler(t) for t in range(num_epochs)])

train(net, train_iter, test_iter, num_epochs, lr,
      custom_callback=LearningRateScheduler(scheduler))

從下圖可以看出,前5步收斂的也很好

Warmup可以被用到任一調度器。有關學習率調度和更多實驗的更詳細討論,請參見 A closer look at deep learning heuristics: learning rate restarts, warmup and distillation。特別是他們發現預熱階段限制了非常深的網絡中參數的發散量。這是符合直覺的,因爲我們預計在訓練網絡的初期,隨機初始化會導致明顯的發散,而這些部分在一開始就需要花費大量的時間進行優化。

總結

  1. 訓練期間降低學習率可以提升準確率,降低模型過擬合;
  2. 在實踐中,當學習進度停滯不前時,逐步降低學習率是有效的。本質上,這保證了我們有效地收斂到一個合適的解,並且只有這樣才能通過降低學習速率來減少參數的變化幅度;
  3. 餘弦調度器在一些計算機視覺問題中很流行;
  4. 優化前的預熱期可以防止發散;
  5. 優化過程在深度學習中有多種作用。除了儘量減少訓練loss,優化算法和學習速率調度的不同選擇會導致測試集上不同程度的泛化和過度擬合。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章