本文爲《深度學習入門 基於Python的理論與實現》的部分讀書筆記
代碼以及圖片均參考此書
目錄
- 參數的更新
- Learning rate decay
- SGD(stochastic gradient descent)
- 介紹更多優化方法之前的預熱
- 指數加權平均(Exponentially weighted average)
- 帶偏差修正的指數加權平均(bias correction in exponentially weighted average)
- Momentum
- Nesterov Momentum
- AdaGrad(Adaptive Gradient)
- RMSprop(root mean square prop)
- Adadelta
- Adam(adaptive momentum estimation)
- 通過實驗比較各個優化方法
參數的更新
本節部分內容除了書本外還參考了以下鏈接!
下面這兩篇博客幫助很大!很多推導都摘自該博客
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
SGD(stochastic gradient descent)
- 這裏指 mini-batch gradient descent
- batch_size 通常設置爲2的冪次方,因爲設置成2的冪次方,更有利於GPU加速。
- η 表示學習率
SGD的缺點:
- SGD依賴於一個較好的初始學習率,且容易陷入局部最優
- 如果函數的形狀非均向(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天的氣溫,於是有:
根據上面的公式我們能夠畫出這條線,如下圖所示:
- 指數加權平均(Exponentially weighted average) 的定義:
理解:
- 由上式可以看出當 時各項的權重之和無限接近於1
- 因爲 ,所以過去個數據之後,權重下降至0.35,可以忽略,相當於對過去個數據的加權平均。
- 當將設爲0.9時,相當於對過去10個數據的加權平均。
下面看下取值不同時曲線的擬合情況:
-
較大時(=0.98 相當於每一點前50天的平均氣溫),曲線波動相對較小更加平滑(綠色曲線),因爲對很多天的氣溫做了平均處理,正因爲如此,曲線還會右移。
-
當 較小時(=0.5 相當於每一點前2天的平均氣溫),曲線波動相對激烈,但是它可以更快的適應溫度的變化。
-
優點:實現簡單且不用存過去個數據的值
帶偏差修正的指數加權平均(bias correction in exponentially weighted average)
- 當我們令=0.98時,我們想得到下圖中的“綠線”,實際上我們得到的是下圖中的“紫線”。對比這兩條線,能夠發現在“紫線”的起點相比“綠線”非常的低,舉個例子,如果第一天的溫度是40℃,按照上面的公式計算,,按照這個公式預估第一天的溫度是0.8℃,顯然距離40℃,差距非常大。同樣,繼續計算,第二天的溫度也沒有很好的擬合,也就是說 指數加權平均 不能很好地擬合前幾天的數據,因此需要偏差修正
- 帶偏差修正的指數加權平均(bias correction in exponentially weighted average)公式爲:
再來按照上述公式對前兩天溫度進行計算,,能夠發現溫度沒有偏差。並且當 時,,這樣在後期 就和沒有修正的指數加權平均一樣了。
Momentum
動量的引入就是爲了加快學習過程,特別是對於高曲率、小但一致的梯度,或者噪聲比較大的梯度能夠很好的加快學習過程。動量的主要思想是積累了之前梯度指數級衰減的移動平均(前面的指數加權平均)
下面用一個圖來對比下,SGD和動量的區別:
區別: SGD每次都會在當前位置上沿着負梯度方向更新(下降,沿着正梯度則爲上升),並不考慮之前的方向梯度大小等等。而動量(moment)通過引入一個新的變量 去積累之前的梯度(通過指數衰減平均得到),達到加速學習過程的目的。
- momentum公式(目前主要有兩種形式,兩種形式應該都可以):
- 第一種(原論文中的公式)
- v對應物理上的速度。式(6.3)表示了物體在梯度方向上受力,在這個力的作用下,物體的速度增加這一物理法則。αv這一項,在物體不受任何力時,承擔使物體逐漸減速的任務(α設定爲0.9 之類的值),對應物理上的地面摩擦或空氣阻力。
- 第二種(pytorch,tensorflow)
- 一般取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相比唯一區別就是多了一步紅色框框起來的步驟
- 但是,我們仔細觀察這個算法,你會發現一個很大的缺點,這個算法會導致運行速度巨慢無比,因爲這個算法每次都要計算,這個相當於又要把fp、bp走一遍。 這樣就導致這個算法的運行速度比Momentum要慢兩倍,因此在實際實現過程中幾乎沒人直接用這個算法,而都是採用了變形版本,該變形版本在形式上近似於Momentum的標準實現.
下面公式爲原始公式
其中爲動量參數,學習率
下面進行公式的變形版本的推導:
令,則
則:
下面就是這種變形版本的精明之處了:
,而且初始速度爲0,即,同時模型在收斂時的速度。也就是說在訓練開始與訓練結束的時候與都近似是相等的,那麼我們直接用去替換就行了,即可以看作是的一個完全等價的替換,我們在訓練的時候根本就不用存儲,一直都只更新就可以了!這樣,我們就可以在存儲的參數值處求梯度,從而避免變量代換。
因此用替換掉上面公式中的,就得出了下面的變形公式(下面出現的其實都表示的是):
可以看出由該變形公式實現的Neterov算法相比於標準的Momentum算法只是在和的係數上有所不同,的係數由變爲 (減小了),的係數由變爲 (增大了),在一定意義上減小了累積速度的作用
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]應該爲,而代碼中用的卻是。這裏我還沒搞清楚爲什麼,等以後在實踐中再慢慢摸索吧!
AdaGrad(Adaptive Gradient)
在神經網絡的學習中,學習率的值很重要。學習率過小,會導致學習花費過多時間;反過來,學習率過大,則會導致學習發散而不能正確進行。在關於學習率的有效技巧中,有一種被稱爲 學習率衰減 learning rate decay 的方法,即隨着學習的進行,使學習率逐漸減小。
AdaGrad會爲參數的每個元素適當地調整學習率,與此同時進行學習
-
保存了以前的所有梯度值的平方和(表示對應矩陣元素的乘法)
-
在更新參數時,通過乘以,就可以調整學習的尺度。這意味着,參數的元素中變動較大(被大幅更新)的元素的學習率將變小。也就是說,可以按參數的元素進行學習率衰減,使變動大的參數的學習率逐漸減小。
然而,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)
是爲了防止分母爲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只累積過去 窗口大小的梯度,其實就是利用指數加權平均,公式如下:
- 時,相當於累積前10個梯度
爲了解決第二個問題,Adadelta最終的公式不需要學習率 。
Adadelta的具體算法如下所示:
- 用來記錄自變量變化量按元素平方的指數加權移動平均
可以看到,AdaDelta和RMSProp的不同之處即在於使用來代替學習率
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,並且使用了偏差修正
算法如下:
- 標準設定值:
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之外的幾個優化方法在這個實驗上的效果都差不多-_-