深度學習入門(五):參數的更新(優化方法)

本文爲《深度學習入門 基於Python的理論與實現》的部分讀書筆記
代碼以及圖片均參考此書

參數的更新

本節部分內容除了書本外還參考了以下鏈接!

下面這兩篇博客幫助很大!很多推導都摘自該博客
https://blog.csdn.net/u012328159/article/details/80252012
https://blog.csdn.net/u012328159/article/details/80311892

https://cs231n.github.io/neural-networks-3/#sgd
https://arxiv.org/pdf/1212.0901v2.pdf
https://zhuanlan.zhihu.com/p/20190387

Learning rate decay

α=11+decayRateepochNumα0\alpha = \frac{1}{1 + decayRate * epochNum} * \alpha_0α=0.95epochNumα0\alpha =0.95^{epochNum} * \alpha_0 α=kepochNumα0\alpha = \frac{k}{\sqrt {epochNum}} * \alpha_0

SGD(stochastic gradient descent)

  • 這裏指 mini-batch gradient descent
  • batch_size 通常設置爲2的冪次方,因爲設置成2的冪次方,更有利於GPU加速。
    在這裏插入圖片描述
  • η 表示學習率

SGD的缺點:

  1. SGD依賴於一個較好的初始學習率,且容易陷入局部最優
    在這裏插入圖片描述
  2. 如果函數的形狀非均向(anisotropic),比如呈延伸狀,搜索的路徑就會非常低效。根本原因是,梯度的方向並沒有指向最小值的方向
    如下圖所示
    在這裏插入圖片描述
    雖然最小值在(x, y) = (0, 0) 處,但是圖6-2 中的梯度在很多地方並沒有指向(0, 0)。,SGD呈“之”字形移動。這是一個相當低效的路徑

在這裏插入圖片描述

class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr

    def update(self, params, grads):
        for key in params:
            params[key] -= self.lr * grads[key]

介紹更多優化方法之前的預熱

指數加權平均(Exponentially weighted average)

在這裏插入圖片描述
如果我們想找一條線去擬合這個數據,該怎麼去做呢。我們知道某一天的氣溫其實和前幾天(前一段時間)相關的,並不是一個獨立的隨機事件,比如夏天氣溫都會普遍偏高些,冬天氣溫普遍都會低一些。我們用[θ1θ2...θn][\theta_{1}、\theta_{2}、...、\theta_{n}]表示第1,2,…,n天的氣溫,於是有:

在這裏插入圖片描述
根據上面的公式我們能夠畫出這條線,如下圖所示:
在這裏插入圖片描述

  • 指數加權平均(Exponentially weighted average) 的定義:
    Vt=βVt1+(1β)θtV_t = \beta V_{t-1} + (1 - \beta)\theta_t
    理解:
    在這裏插入圖片描述
  • 由上式可以看出當 tt\rightarrow\infty 時各項的權重之和無限接近於1
  • 因爲 ϵ11ϵ1e0.35\epsilon^{\frac{1}{1 - \epsilon}} \approx \frac{1}{e} \approx 0.35,所以過去11β\frac{1}{1 - \beta}個數據之後,權重下降至0.35,可以忽略,相當於對過去11β\frac{1}{1 - \beta}個數據的加權平均。
  • 當將β\beta設爲0.9時,相當於對過去10個數據的加權平均。

下面看下β\beta取值不同時曲線的擬合情況:
在這裏插入圖片描述

  • β\beta 較大時(β\beta=0.98 相當於每一點前50天的平均氣溫),曲線波動相對較小更加平滑(綠色曲線),因爲對很多天的氣溫做了平均處理,正因爲如此,曲線還會右移。

  • β\beta 較小時(β\beta=0.5 相當於每一點前2天的平均氣溫),曲線波動相對激烈,但是它可以更快的適應溫度的變化。

  • 優點:實現簡單且不用存過去11β\frac{1}{1 - \beta}個數據的值

帶偏差修正的指數加權平均(bias correction in exponentially weighted average)

  • 當我們令β\beta=0.98時,我們想得到下圖中的“綠線”,實際上我們得到的是下圖中的“紫線”。對比這兩條線,能夠發現在“紫線”的起點相比“綠線”非常的低,舉個例子,如果第一天的溫度是40℃,按照上面的公式計算,V1=0.98V0+(10.98)θ1=0.980+0.0240=0.8V_1=0.98V_0+(1−0.98)θ_1=0.98∗0+0.02∗40=0.8,按照這個公式預估第一天的溫度是0.8℃,顯然距離40℃,差距非常大。同樣,繼續計算,第二天的溫度也沒有很好的擬合,也就是說 指數加權平均 不能很好地擬合前幾天的數據,因此需要偏差修正
    在這裏插入圖片描述
  • 帶偏差修正的指數加權平均(bias correction in exponentially weighted average)公式爲:

在這裏插入圖片描述

再來按照上述公式對前兩天溫度進行計算,V1=V11β1=0.810.98=40V_1 = \frac{V_1}{1 - \beta^1} = \frac{0.8}{1 -0.98} = 40,能夠發現溫度沒有偏差。並且當tt \rightarrow \infty 時,βt0\beta^t \rightarrow 0,這樣在後期 VtV_t就和沒有修正的指數加權平均一樣了。

Momentum

動量的引入就是爲了加快學習過程,特別是對於高曲率、小但一致的梯度,或者噪聲比較大的梯度能夠很好的加快學習過程。動量的主要思想是積累了之前梯度指數級衰減的移動平均(前面的指數加權平均)
下面用一個圖來對比下,SGD和動量的區別:
在這裏插入圖片描述
區別: SGD每次都會在當前位置上沿着負梯度方向更新(下降,沿着正梯度則爲上升),並不考慮之前的方向梯度大小等等。而動量(moment)通過引入一個新的變量 vv 去積累之前的梯度(通過指數衰減平均得到),達到加速學習過程的目的。
在這裏插入圖片描述

  • momentum公式(目前主要有兩種形式,兩種形式應該都可以):
  1. 第一種(原論文中的公式)
    在這裏插入圖片描述
  • v對應物理上的速度。式(6.3)表示了物體在梯度方向上受力,在這個力的作用下,物體的速度增加這一物理法則。αv這一項,在物體不受任何力時,承擔使物體逐漸減速的任務(α設定爲0.9 之類的值),對應物理上的地面摩擦或空氣阻力。
  1. 第二種(pytorch,tensorflow)
    在這裏插入圖片描述
  • β\beta一般取0.9
  • 移動指數加權平均來對網絡參數平滑處理,讓梯度的擺動幅度變小
  • β=0.9\beta = 0.9時,10次更新之後就過了初始階段,因此不用bias correction

在這裏插入圖片描述

  • 下面以第一種形式進行實現:
class Momentum:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None

    def update(self, params, grads):
        # initialization
        if self.v is None:
            self.v = {}
            for key in params:
                self.v[key] = np.zeros_like(params[key])

        for key in params:
            self.v[key] =  self.momentum * self.v[key] - self.lr * grads[key]
            params[key] += self.v[key]

Nesterov Momentum

Nesterov Momentum是對Momentum的改進,可以理解爲nesterov動量在標準動量方法中添加了一個校正因子。用一張圖來形象的對比下momentum和nesterov momentum的區別:
在這裏插入圖片描述

Nesterov momentum. Instead of evaluating gradient at the current position (red circle), we know that our momentum is about to carry us to the tip of the green arrow. With Nesterov momentum we therefore instead evaluate the gradient at this “looked-ahead” position.

下面對比一下Momentum和Nesterov:

  • Momentum
    在這裏插入圖片描述
  • Nesterov
    在這裏插入圖片描述
  • 可以看出Nesterov和Momentum相比唯一區別就是多了一步紅色框框起來的步驟
  • 但是,我們仔細觀察這個算法,你會發現一個很大的缺點,這個算法會導致運行速度巨慢無比,因爲這個算法每次都要計算θiL(f(x(i);θ+αv),y(i))\nabla_\theta\sum_iL(f(x^{(i)};\theta + \alpha v),y^{(i)}),這個相當於又要把fp、bp走一遍。 這樣就導致這個算法的運行速度比Momentum要慢兩倍,因此在實際實現過程中幾乎沒人直接用這個算法,而都是採用了變形版本,該變形版本在形式上近似於Momentum的標準實現.

下面公式爲原始公式

  • vt+1=βvtαθtL(θt+βvt)v_{t+1} = \beta v_{t} - \alpha \nabla_{\theta_t} L(\theta_t + \beta v_t)
    θt+1=θt+vt+1=θt+βvtαθtL(θt+βvt)\theta_{t+1} = \theta_{t} + v_{t+1} = \theta_{t} + \beta v_{t} - \alpha \nabla_{\theta_t} L(\theta_t + \beta v_t)
    其中β\beta爲動量參數,α\alpha學習率

下面進行公式的變形版本的推導:
θt=θt+βvt\theta_t^{'} = \theta_t + \beta v_t,則vt+1=βvtαθtL(θt)v_{t+1} = \beta v_{t} - \alpha \nabla_{\theta_t} L(\theta_t^{'})
則:
θt+1=θt+1+βvt+1=θt+vt+1+βvt+1=θt+(1+β)vt+1=θtβvt+(1+β)[βvtαθtL(θt)]=θt+β2vt(1+β)αθtL(θt)\begin{aligned} \theta_{t+1}^{'} &= \theta_{t+1} + \beta v_{t+1} \\&= \theta_t + v_{t+1} + \beta v_{t+1} \\&= \theta_t + (1 + \beta)v_{t+1} \\&=\theta_{t}^{'} - \beta v_t + (1 + \beta)[\beta v_t - \alpha \nabla_{\theta_t} L(\theta_t^{'})] \\&= \theta_{t}^{'} + \beta^2 v_t - (1 + \beta) \alpha \nabla_{\theta_t} L(\theta_t^{'}) \end{aligned}

下面就是這種變形版本的精明之處了:
θt=θt+βvt\theta_t^{'} = \theta_t + \beta v_t,而且初始速度爲0,即v1=0v_1 = 0,同時模型在收斂時的速度vt0v_t \approx 0。也就是說在訓練開始與訓練結束的時候θt\theta_t^{'}θt\theta_t都近似是相等的,那麼我們直接用θt\theta_t^{'}去替換θt\theta_t就行了,即θt\theta_t^{'}可以看作是θt\theta_t的一個完全等價的替換,我們在訓練的時候根本就不用存儲θt\theta_t,一直都只更新θt\theta_t^{'}就可以了!這樣,我們就可以在存儲的參數值處求梯度,從而避免變量代換。

因此用θt\theta_t替換掉上面公式中的θt\theta_t{'},就得出了下面的變形公式(下面出現的θt\theta_t其實都表示的是θt\theta_t^{'}):
vt+1=βvtαθtL(θt)v_{t+1} = \beta v_{t} - \alpha \nabla_{\theta_t} L(\theta_t)θt+1=θt+β2vt(1+β)αθtL(θt)=θt+βvt+1αθtL(θt)\begin{aligned} \theta_{t+1} &= \theta_{t} + \beta^2 v_t - (1 + \beta) \alpha \nabla_{\theta_t} L(\theta_t) \\&=\theta_t + \beta v_{t+1} - \alpha \nabla_{\theta_t} L(\theta_t) \end{aligned}

可以看出由該變形公式實現的Neterov算法相比於標準的Momentum算法只是在vtv_tθtL(θt)\nabla_{\theta_t} L(\theta_t)的係數上有所不同,vtv_t的係數由β\beta變爲β2\beta^2 (減小了),θtL(θt)\nabla_{\theta_t} L(\theta_t)的係數由α-\alpha變爲(1+β)α-(1 + \beta) \alpha (增大了),在一定意義上減小了累積速度的作用

This anticipatory update prevents us from going too fast and results in increased responsiveness, which has significantly increased the performance of RNNs on a number of tasks.
It enjoys stronger theoretical converge guarantees for convex functions and in practice it also consistenly works slightly better than standard momentum.

class Neterov:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None

    def update(self, params, grads):
        # initialization
        if self.v is None:
            self.v = {}
            for key in params:
                self.v[key] = np.zeros_like(params[key])

        for key in params:
            self.v[key] =  self.momentum * self.v[key] - self.lr * grads[key]
            params[key] += self.momentum * self.v[key] - self.lr * grads[key]

原書代碼中的參數更新其實是這樣的:

self.v[key] *= self.momentum
self.v[key] -= self.lr * grads[key]
params[key] += self.momentum * self.momentum * self.v[key]
params[key] -= (1 + self.momentum) * self.lr * grads[key]

這與上面推出的變形公式實現是不一樣的,按公式來的話第三行中的self.v[key]應該爲vtv_t,而代碼中用的卻是vt+1v_{t+1}。這裏我還沒搞清楚爲什麼,等以後在實踐中再慢慢摸索吧!

AdaGrad(Adaptive Gradient)

在神經網絡的學習中,學習率的值很重要。學習率過小,會導致學習花費過多時間;反過來,學習率過大,則會導致學習發散而不能正確進行。在關於學習率的有效技巧中,有一種被稱爲 學習率衰減 learning rate decay 的方法,即隨着學習的進行,使學習率逐漸減小。
AdaGrad會爲參數的每個元素適當地調整學習率,與此同時進行學習
在這裏插入圖片描述

  • hh保存了以前的所有梯度值的平方和(\odot表示對應矩陣元素的乘法)

  • 在更新參數時,通過乘以1h\frac {1}{\sqrt{h}},就可以調整學習的尺度。這意味着,參數的元素中變動較大(被大幅更新)的元素的學習率將變小。也就是說,可以按參數的元素進行學習率衰減,使變動大的參數的學習率逐漸減小。

然而,AdaGrad的缺點也很明顯:

  • 學習越深入,更新的幅度就越小。實際上,如果無止境地學習,更新量就會變爲0,完全不再更新。

在這裏插入圖片描述

class AdaGrad:
    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None

    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key in params:
                self.h[key] = np.zeros_like(params[key])

        for key in params:
            self.h[key] += grads[key] ** 2
            params[key] -= self.lr * grads[key] / (self.h[key] ** 0.5 + 1e-7) # 別忘了加上1e-7這個微小值!!!

RMSprop(root mean square prop)

hβh+(1β)(LWLW)h \leftarrow \beta h + (1 - \beta) (\frac {\partial L}{\partial W} \odot \frac {\partial L}{\partial W})WWα1h+ϵLWW \leftarrow W - \alpha \frac {1}{\sqrt{h} + \epsilon} \frac {\partial L}{\partial W}
ϵ\epsilon是爲了防止分母爲0,取 1e−7

  • RMSProp 方法並不是將過去所有的梯度一視同仁地相加,而是逐漸
    地遺忘過去的梯度,從而改善AdaGrad學習率的過早衰減問題
  • This optimizer is usually a good choice for recurrent neural networks
class RMSprop:
    def __init__(self, lr=0.01, decay_rate=0.99):
        self.lr = lr
        self.decay_rate = decay_rate
        self.h = None

    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key in params:
                self.h[key] = np.zeros_like(params[key])

        for key in params:
            self.h[key] = self.h[key] * self.decay_rate + (1 - self.decay_rate) * grads[key] ** 2
            params[key] -= self.lr * grads[key] / (self.h[key] ** 0.5 + 1e-7) # 別忘了加上1e-7這個微小值!!!

Adadelta

Adadelta是對Adagrad的改進,主要是爲了克服Adagrad的兩個缺點:

  • the continual decay of learning rates throughout training
  • the need for a manually selected global learning rate

爲了解決第一個問題,Adadelta只累積過去 ww 窗口大小的梯度,其實就是利用指數加權平均,公式如下:
st+1ρst+(1ρ)(LWLW)s_{t+1} \leftarrow \rho s_t + (1 - \rho) (\frac {\partial L}{\partial W} \odot \frac {\partial L}{\partial W})

  • ρ=0.9\rho = 0.9時,相當於累積前10個梯度

爲了解決第二個問題,Adadelta最終的公式不需要學習率 α\alpha

Adadelta的具體算法如下所示:

st+1ρst+(1ρ)(gt+1gt+1)s_{t+1} \leftarrow \rho s_t + (1 - \rho) (g_{t+1} \odot g_{t+1})gt+1Δxt+ϵst+1+ϵgt+1g_{t+1}^{'} \leftarrow \sqrt {\frac {\Delta x_{t} + \epsilon}{s_{t+1} + \epsilon}} \odot g_{t+1}xt+1xtgt+1x_{t+1} \leftarrow x_t - g_{t+1}^{'}Δxt+1ρΔxt+(1ρ)gt+1gt+1\Delta x_{t+1} \leftarrow \rho \Delta x_t + (1 - \rho) g_{t+1}^{'} \odot g_{t+1}^{'}

  • Δxt\Delta x_{t}用來記錄自變量變化量gtg_{t}^{'}按元素平方的指數加權移動平均

可以看到,AdaDelta和RMSProp的不同之處即在於使用Δxt\sqrt {\Delta x_{t}}來代替學習率

class AdaDelta:
    def __init__(self, rho=0.9):
        self.rho = rho
        self.s = None
        self.delta = None

    def update(self, params, grads):
        if self.s is None:
            self.s = {}
            self.delta = {}
            for key in params:
                self.s[key] = np.zeros_like(params[key])
                self.delta[key] = np.zeros_like(params[key])

        for key in params:
            eps = 1e-7
            self.s[key] = self.rho * self.s[key] + (1 - self.rho) * (grads[key] ** 2)
            g = np.sqrt(self.delta[key] + eps) / np.sqrt(self.s[key] + eps) * grads[key]
            params[key] -= g
            self.delta[key] = self.rho * self.delta[key] + (1 - self.rho) * (g ** 2)

Adam(adaptive momentum estimation)

論文地址:http://arxiv.org/abs/1412.6980v8

Adam結合了Momentum與RMSProp,並且使用了偏差修正
算法如下:
vt+1β1vt+(1β1)gt+1v_{t+1} \leftarrow \beta_1 v_t + (1 - \beta_1)g_{t+1}st+1β2st+(1β2)gt+1gt+1s_{t+1} \leftarrow \beta_2 s_t + (1 - \beta_2)g_{t+1} \odot g_{t+1} vt+1correctvt+11β1t+1v_{t+1}^{correct} \leftarrow \frac {v_{t+1}}{1 - \beta_1^{t+1}}st+1correctst+11β2t+1s_{t+1}^{correct} \leftarrow \frac {s_{t+1}}{1 - \beta_2^{t+1}}gt+1αvt+1correctst+1correct+ϵg_{t+1}^{'} \leftarrow \frac {\alpha v_{t+1}^{correct}}{\sqrt {s_{t+1}^{correct}} + \epsilon}xt+1xtgt+1x_{t+1} \leftarrow x_t - g_{t+1}^{'}

  • 標準設定值:α=0.001,β1=0.9,β2=0.999,ϵ=108\alpha = 0.001,\beta_1 = 0.9, \beta_2 = 0.999, \epsilon = 10^{-8}
    在這裏插入圖片描述
class Adam:
    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.v = None
        self.s = None
        self.iter = 0 # 更新次數

    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            self.s = {}
            for key in params:
                self.v[key] = np.zeros_like(params[key])
                self.s[key] = np.zeros_like(params[key])

        self.iter += 1

        for key in params:
            self.v[key] = self.beta1 * self.v[key] + (1 - self.beta1) * grads[key]
            self.s[key] = self.beta2 * self.s[key] + (1 - self.beta2) * (grads[key]**2)

            unbias_v = self.v[key] / (1 - self.beta1 ** (self.iter))
            unbias_s = self.s[key] / (1 - self.beta2 ** (self.iter))

            params[key] -= self.lr * unbias_v / (np.sqrt(unbias_s) + 1e-8)

通過實驗比較各個優化方法

在Mnist數據集上用不同的優化方法進行訓練

import sys
file_path = __file__.replace('\\', '/')
dir_path = file_path[: file_path.rfind('/')] # 當前文件夾的路徑
pardir_path = dir_path[: dir_path.rfind('/')]
sys.path.append(pardir_path) # 添加上上級目錄到python模塊搜索路徑

import numpy as np
from dataset.mnist import load_mnist
from trainer.trainer import Trainer
from layer.multi_layer_net import MultiLayerNet
import matplotlib.pyplot as plt

(x_train, t_train),  (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=True, shuffle_data=True)

# setting
train_flag = 1 # 進行訓練還是預測
gradcheck_flag = 0 # 對已訓練的網絡進行梯度檢驗

pkl_file_name = dir_path + '/optimizer_compare'
fig_name = dir_path + '/optimizer_compare'

optimizers = ['sgd', 'momentum', 'nesterov', 'adagrad', 'rmsprpo', 'adadelta', 'adam']
nets = {}
trainers = {}
results_val, results_train = {}, {}

for optimizer in optimizers[:]:
    nets[optimizer] = MultiLayerNet(784, [100, 100, 100, 100], 10,
                    activation='relu', weight_init_std='relu', weight_decay_lambda=0, 
                    use_dropout=False, dropout_ration=0.5, use_batchnorm=True, 
                    pretrain_flag=False, pkl_file_name=pkl_file_name + '_' + optimizer + '.pkl')

    trainers[optimizer] = Trainer(nets[optimizer], x_train, t_train, x_test, t_test,
                    epochs=3, mini_batch_size=100,
                    optimizer=optimizer, optimizer_param={}, 
                    save_model_flag=False, pkl_file_name=pkl_file_name + '_' + optimizer + '.pkl', plot_flag=False, fig_name=fig_name + '_' + optimizer + '.png',
                    evaluate_sample_num_per_epoch=None, verbose=True)
    trainers[optimizer].train()
    results_val[optimizer] = trainers[optimizer].test_acc_list
    results_train[optimizer] = trainers[optimizer].train_acc_list

fig, axes = plt.subplots(1, len(optimizers))
for i, optimizer in enumerate(optimizers):
    axes[i].set_title(optimizer)
    axes[i].set_ylim(0, 1)
    if i > 0:
        axes[i].set_yticks([])
    x = np.arange(len(results_val[optimizer]))
    axes[i].plot(x, results_val[optimizer])
    axes[i].plot(x, results_train[optimizer], '--')

plt.savefig(fig_name)
plt.show()

得到的訓練結果如下所示:
在這裏插入圖片描述

  • 從圖片上看好像除了SGD之外的幾個優化方法在這個實驗上的效果都差不多-_-
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章