莫煩大大TensorFlow學習筆記(8)----優化器


一、TensorFlow中的優化器

  1. tf.train.GradientDescentOptimizer:梯度下降算法
  2. tf.train.AdadeltaOptimizer
  3. tf.train.AdagradOptimizer
  4. tf.train.MomentumOptimizer:動量梯度下降算法
  5. tf.train.AdamOptimizer:自適應矩估計優化算法
  6. tf.train.RMSPropOptimizer
  7. tf.train.AdagradDAOptimizer
  8. tf.train.FtrlOptimizer
  9. tf.train.ProximalGradientDescentOptimizer
  10. tf.train.ProximalAdagradOptimizertf.train.RMSProOptimizer

(1)如果數據是稀疏的,使用自適應學習方法。
(2)RMSprop,Adadelta,Adam是非常相似的優化算法,Adam的bias-correction幫助其在最后優化期間梯度變稀疏的情況下略微戰勝了RMSprop。整體來講,Adam是最好的選擇。
(3)很多論文中使用vanilla SGD without momentum。SGD通常能找到最小值,但是依賴健壯的初始化,並且容易陷入鞍點。因此,如果要獲得更快的收斂速度和訓練更深更復雜的神經網絡,需要選擇自適應學習方法。

https://blog.csdn.net/winycg/article/details/79363169

 

二、常用的種類:

1、tf.train.Optimizer:

class tf.train.Optimizer:優化器(optimizers)類的基類。
Optimizer基類提供了計算損失梯度的方法,並將梯度應用於變量。這個類定義了在訓練模型的時候添加一個操作的API。你基本上不會直接使用這個類,但是你會用到他的子類比如GradientDescentOptimizer, AdagradOptimizer, MomentumOptimizer.等等這些。 

2、tf.train.GradientDescentOptimizer:梯度下降

原理:

  • batch GD【全部樣本,速度慢】

  • 隨機GD【隨機一個樣本,速度快,但局部最優】

  • mini-batch GD 【batch個樣本,常在數據量較大時使用】

訓練集樣本數少【≤2000】:采用batchGD

訓練集樣本數多:采用mini-batch GD,batch大小一般為64-512. 訓練時多嘗試一下2的次方來找到最合適的batch大小。

 

 

這個類是實現梯度下降算法的優化器。這個構造函數需要的一個學習率就行了。

構造函數:tf.train.GradientDescentOptimizer(0.001).minimize(loss,global_step=None,var_list=None,gate_gradients=GATE_OP,aggregation_method=None,colocate_gradients_with_ops=False,name=None,grad_loss=None)

1 __init__(
2 
3     learning_rate,
4 
5     use_locking=False,
6 
7     name='GradientDescent'
8 
9 )
View Code

learning_rate: (學習率)張量或者浮點數

use_locking: 為True時鎖定更新

name: 梯度下降名稱,默認為"GradientDescent".

 

3、tf.train.AdadeltaOptimizer:

實現了 Adadelta算法的優化器,可以算是下面的Adagrad算法改進版本。

構造函數: tf.train.AdadeltaOptimizer.init(learning_rate=0.001, rho=0.95, epsilon=1e-08, use_locking=False, name=’Adadelta’)

4、tf.train.AdagradOptimizer:

構造函數:tf.train.AdagradOptimizer.__init__(learning_rate, initial_accumulator_value=0.1, use_locking=False, name=’Adagrad’)

5、tf.train.MomentumOptimizer:

原理:

momentum表示要在多大程度上保留原來的更新方向,這個值在0-1之間,在訓練開始時,由於梯度可能會很大,所以初始值一般選為0.5;當梯度不那么大時,改為0.9。 α是學習率,即當前batch的梯度多大程度上影響最終更新方向,跟普通的SGD含義相同。

應用:

構造函數:tf.train.MomentumOptimizer.__init__(learning_rate, momentum, use_locking=False, name=’Momentum’, use_nesterov=False)

 1 __init__(
 2 
 3     learning_rate,
 4 
 5     momentum,
 6 
 7     use_locking=False,
 8 
 9     name='Momentum',
10 
11     use_nesterov=False
12 
13 )
View Code

learning_rate: (學習率)張量或者浮點數

momentum: (動量)張量或者浮點數

use_locking: 為True時鎖定更新

name:  梯度下降名稱,默認為 "Momentum".

use_nesterov:  為True時,使用 Nesterov Momentum.

 

6、tf.train.RMSPropOptimizer

目的和動量梯度一樣,減小垂直方向,增大水平方向。W為水平方向,b為垂直方向。

 

 

7、tf.train.AdamOptimizer:動量和RMSProp結合

應用:

 1 __init__(
 2 
 3     learning_rate=0.001,
 4 
 5     beta1=0.9,
 6 
 7     beta2=0.999,
 8 
 9     epsilon=1e-08,
10 
11     use_locking=False,
12 
13     name='Adam'
14 
15 )
View Code

構造函數:tf.train.AdamOptimizer.__init__(learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-08, use_locking=False, name=’Adam’)

learning_rate: (學習率)張量或者浮點數,需要調試

beta1:  浮點數或者常量張量 ,表示 The exponential decay rate for the 1st moment estimates.【推薦使用0.9】

beta2:  浮點數或者常量張量 ,表示 The exponential decay rate for the 2nd moment estimates.【推薦使用0.999】

epsilon: A small constant for numerical stability. This epsilon is "epsilon hat" in the Kingma and Ba paper (in the formula just before Section 2.1), not the epsilon in Algorithm 1 of the paper.

use_locking: 為True時鎖定更新

name:  梯度下降名稱,默認為 "Adam".

 

8、Lookahead、LazyOptimizer、MaskedAdamOptimizer、AdaBound(轉載)

(1)AdaBound算法:像Adam一樣快,又像SGD一樣好的優化器

論文地址

GitHub地址:(Pytorch)

GitHub地址:(Tensorflow)

SGD的缺點:

SGD現在后期調優時還是經常使用到,但SGD的問題是前期收斂速度慢。SGD前期收斂慢的原因: SGD在更新參數時對各個維度上梯度的放縮是一致的,並且在訓練數據分布極不均很時訓練效果很差。而因為收斂慢的問題應運而生的自適應優化算法Adam、AdaGrad、RMSprop 等,但這些自適應的優化算法雖然可以在訓練早期展現出快速的收斂速度,但其在測試集上的表現卻會很快陷入停滯,並最終被 SGD 超過。

Adam等自適應學習率算法缺點:

這就是目前很多大牛任然喜歡SGD的原因。這篇文章對於Adam后期的毛病進行了分析,原因出在自適應方法訓練后期不穩定的極端學習率。換句話說,就是自適應學習率訓練到后期,學習率出現極端情況,更新參數時有些維度上學習率特別大,有些維度學習率特別小。

 

 采樣參數的學習率,每個單元格包含一個通過對學習率進行數值運算得到的值,顏色越淺代表學習率越小。

我們可以看到,當模型接近收斂時,學習率中有大量的極端值(包含許多小於 0.01 和大於 1000 的情況)。這一現象表明在實際訓練中,極端學習率是實際存在的。

發現這個問題怎么解決?如何融合上面兩種方法的優點?

那就對自適應學習率加一下限制吧。具體做法是對學習率進行動態裁剪,在這一設置下,在訓練早期由於上下界對學習率的影響很小,算法更加接近於 Adam;而隨着時間增長裁減區間越來越收緊,模型的學習率逐漸趨於穩定,在末期更加貼近於 SGD。AMSBound 可以對 AMSGrad 采用類似的裁剪得到。

 

換句話說,Adam和SGD是AdaBound的特殊情況。

在這一設置下,在訓練早期由於上下界對學習率的影響很小,算法更加接近於 Adam;而隨着時間增長裁減區間越來越收緊,模型的學習率逐漸趨於穩定,在末期更加貼近於 SGD。AMSBound 可以對 AMSGrad 采用類似的裁剪得到。

(2)Lookahead

論文地址

GitHub地址:(Pytorch)

GitHub地址:(Tensorflow)

Lookahead的思路很朴素,准確來說它並不是一個優化器,而是一個使用現有優化器的方案。簡單來說它就是下面三個步驟的循環執行:

附:《機器之心的Lookahead的介紹》

 

(3)LazyAdam、MaskedAdamOptimize

 

LazyAdam
和圖像等領域不同,對 NLU 之類的任務,每個 batch 采樣到的詞有限,每次更新對 Embedding 的梯度估計都是稀疏的。非 momentum-based 的 Optimizer 每步只會更新采樣到的詞,而對於所有帶動量的優化器(自然也就包括Adam以及帶動量的SGD)都存在一個問題:當前batch中沒被采樣到的詞,依然會使用歷史動量來更新,這可能導致Embedding層過擬合。具體來說,當一個詞的被采樣過后,它的Embedding的梯度不為0,這個梯度也會被記錄在動量中,實際更新是用動量去更新的;在后面的batch中,假如該詞沒有被采樣到,它的Embedding的梯度為0,但是它的動量並不為0,所以該詞還是被更新了。這樣一來就算沒有被反復采樣的詞,對應的Embedding也被反復更新了,就導致了過擬合。

 

所以,一個改進的方案是只有當該詞被采樣過才更新,這就是LazyOptimizer的基本原理了。

 

LazyAdam是Adam的變體,可以更有效地處理稀疏更新。原始的Adam算法為每個可訓練變量維護兩個移動平均累加器,累加器在每一步都會更新**。 而此類為稀疏變量提供了更加懶惰的梯度更新處理,它僅更新當前batch中出現的稀疏變量索引的移動平均累加器,而不是更新所有索引的累加器。 與原始的Adam優化器相比,它可以為某些應用提供模型訓練吞吐量的大幅改進。 但是它的語義與原始的Adam算法略有不同,可能會導致不同的實驗結果。

 

在實現上,我們要如何判斷一個詞有沒有被采樣過呢?當然終極方法肯定是傳入被采樣過的詞的index了,但這使用上不夠友好。我這里使用了一個近似的方法:判斷該詞的Embedding對應的梯度是否為0,如果為0意味着它“很可能”在當前batch沒有被采樣到。背后的原理在於,如果它沒有被采樣到,那么梯度一定為0,如果它被采樣到了,那么梯度為0的概率是非常小的,畢竟那么多分量,同時為0的可能性很小,所以這樣實現也夠用了。

 

AdamOptimizer源碼中函數_apply_sparse和_resource_apply_sparse 主要用在稀疏向量的更新操作上,而具體的實現是在函數_apply_sparse_shared中

LazyAdam源碼:

可以看出公式與Adam都相同,不同的是每次迭代根據當前batch的indices來對一階動量和二階動量進行更新。

 

def _apply_sparse(self, grad, Var):
    beta1_power, beta2_power = self._get_beta_accumulators()
    beta1_power = math_ops.cast(beta1_power, Var.dtype.base_dtype)
    beta2_power = math_ops.cast(beta2_power, Var.dtype.base_dtype)
    lr_t = math_ops.cast(self._lr_t, Var.dtype.base_dtype)
    beta1_t = math_ops.cast(self._beta1_t, Var.dtype.base_dtype)
    beta2_t = math_ops.cast(self._beta2_t, Var.dtype.base_dtype)
    epsilon_t = math_ops.cast(self._epsilon_t, Var.dtype.base_dtype)
    lr = (lr_t * math_ops.sqrt(1 - beta2_power) / (1 - beta1_power))
 
    # \\(m := beta1 * m + (1 - beta1) * g_t\\)
    m = self.get_slot(Var, "m")
    m_t = state_ops.scatter_update(m, grad.indices,
                                   beta1_t * array_ops.gather(m, grad.indices) +
                                   (1 - beta1_t) * grad.Values,
                                   use_locking=self._use_locking)#一階動量
 
    # \\(V := beta2 * V + (1 - beta2) * (g_t * g_t)\\)
    V = self.get_slot(Var, "V")
    V_t = state_ops.scatter_update(V, grad.indices,
                                   beta2_t * array_ops.gather(V, grad.indices) +
                                   (1 - beta2_t) * math_ops.square(grad.Values),
                                   use_locking=self._use_locking) #二階動量
 
    # \\(Variable -= learning_rate * m_t / (epsilon_t + sqrt(V_t))\\)
    m_t_slice = array_ops.gather(m_t, grad.indices)
    V_t_slice = array_ops.gather(V_t, grad.indices)
    denominator_slice = math_ops.sqrt(V_t_slice) + epsilon_t
    Var_update = state_ops.scatter_sub(Var, grad.indices,
                                       lr * m_t_slice / denominator_slice,
                                       use_locking=self._use_locking)
    return control_flow_ops.group(Var_update, m_t, V_t)
View Code

 

Madam:

from tensorflow.python.ops import array_ops
from tensorflow.python.training import adam
from tensorflow.python.framework import ops
from tensorflow.python.ops import control_flow_ops
from tensorflow.python.ops import math_ops
from tensorflow.python.ops import resource_variable_ops
from tensorflow.python.ops import state_ops
from tensorflow.python.ops import variable_scope
from tensorflow.python.training import optimizer
 
class MaskedAdamOptimizer(adam.AdamOptimizer):
    def _apply_sparse_shared(self, grad, var, indices, scatter_add):
        beta1_power, beta2_power = self._get_beta_accumulators()
        beta1_power = math_ops.cast(beta1_power, var.dtype.base_dtype)
        beta2_power = math_ops.cast(beta2_power, var.dtype.base_dtype)
        lr_t = math_ops.cast(self._lr_t, var.dtype.base_dtype)
        beta1_t = math_ops.cast(self._beta1_t, var.dtype.base_dtype)
        beta2_t = math_ops.cast(self._beta2_t, var.dtype.base_dtype)
        epsilon_t = math_ops.cast(self._epsilon_t, var.dtype.base_dtype)
        lr = (lr_t * math_ops.sqrt(1 - beta2_power) / (1 - beta1_power))
        # m_t = beta1 * m + (1 - beta1) * g_t
        m = self.get_slot(var, "m")
        m_scaled_g_values = grad * (1 - beta1_t)
        m_t = state_ops.assign(m, m * beta1_t,
                               use_locking=self._use_locking)
        with ops.control_dependencies([m_t]):
            m_t = scatter_add(m, indices, m_scaled_g_values)
        # v_t = beta2 * v + (1 - beta2) * (g_t * g_t)
        v = self.get_slot(var, "v")
        v_scaled_g_values = (grad * grad) * (1 - beta2_t)
        v_t = state_ops.assign(v, v * beta2_t, use_locking=self._use_locking)
        with ops.control_dependencies([v_t]):
            v_t = scatter_add(v, indices, v_scaled_g_values)
        gather_m_t = array_ops.gather(m_t, indices)
        gather_v_t = array_ops.gather(v_t, indices)
        gather_v_sqrt = math_ops.sqrt(gather_v_t)
        var_update = scatter_add(var, indices, -lr * gather_m_t / (gather_v_sqrt + epsilon_t))
        return control_flow_ops.group(*[var_update, m_t, v_t])
View Code

兩者在計算移動平均累加器時(一階動量和二階動量)有所不同:

LazyAdam:

 

m_t = state_ops.scatter_update(m, grad.indices,
                                   beta1_t * array_ops.gather(m, grad.indices) +
                                   (1 - beta1_t) * grad.Values,
                                   use_locking=self._use_locking)
View Code

Madam:

m_scaled_g_Values = grad * (1 - beta1_t)
        m_t = state_ops.assign(m, m * beta1_t,
                               use_locking=self._use_locking)  
        with ops.control_dependencies([m_t]):
            m_t = scatter_add(m, indices, m_scaled_g_Values)
View Code

Madam其實是介於Lazy Adam和 Adam之間的一種方法,其與Lazy Adam唯一的不同在於對一階動量m和二階動量 V 進行 decay 的操作,Madam是全部都要 decay,即當前batch沒有采樣到的變量所對應的之前動量的累積值也要考慮。 而LazyAdam 是只 decay 采樣到的embedding。(在計算指數加權平均時,LazyAdam只對當前采樣到的變量之前的平均值進行累加,沒有采樣到的樣本不累加,而Madam要全部累加)。

LazyAdam存在的一個問題是當梯度為0時不更新對應的m和v。實際上當其他權重改變時m和v應該更新。Madam應該是解決了這個問題所以性能變得更好。

為了更形象的說明它們的差異,通過一個假設的例子來說明,用一階動量來舉例:

 

MaskedAdamOptimize

鏈接:https://www.zhihu.com/question/265357659/answer/580469438
來源:知乎


AllenNLP 也在 2018 EMNLP 的 Tutorial 里面提到。

和圖像等領域不同,對 NLU 之類的任務,每個 batch 采樣到的詞有限,每次更新對 Embedding 的梯度估計都是稀疏的。非 momentum-based 的 Optimizer 每步只會更新采樣到的詞,而對於 momentum-based 的 Optimizer,現在所有框架的實現都會用當前的 momentum 去更新所有的詞,即使這些詞在連續的幾十步更新里都沒有被采樣到。這可能會使 Embedding 過擬合。

下面是一個文本分類問題在不同 setting 下對應的 valid set 准確率曲線。learning rate 固定不變。madam 指的是修正過后的 adam,LoEmbed 表示是否加載預訓練的詞向量。在加載詞向量的 setting 下,不加修正的 Adam 過擬合的不能看。

代碼:

# for tensorflow 1.12.0
from tensorflow.python.ops import array_ops
from tensorflow.python.training import adam
from tensorflow.python.framework import ops
from tensorflow.python.ops import control_flow_ops
from tensorflow.python.ops import math_ops
from tensorflow.python.ops import resource_variable_ops
from tensorflow.python.ops import state_ops
from tensorflow.python.ops import variable_scope
from tensorflow.python.training import optimizer

class MaskedAdamOptimizer(adam.AdamOptimizer):
    def _apply_sparse_shared(self, grad, var, indices, scatter_add):
        beta1_power, beta2_power = self._get_beta_accumulators()
        beta1_power = math_ops.cast(beta1_power, var.dtype.base_dtype)
        beta2_power = math_ops.cast(beta2_power, var.dtype.base_dtype)
        lr_t = math_ops.cast(self._lr_t, var.dtype.base_dtype)
        beta1_t = math_ops.cast(self._beta1_t, var.dtype.base_dtype)
        beta2_t = math_ops.cast(self._beta2_t, var.dtype.base_dtype)
        epsilon_t = math_ops.cast(self._epsilon_t, var.dtype.base_dtype)
        lr = (lr_t * math_ops.sqrt(1 - beta2_power) / (1 - beta1_power))
        # m_t = beta1 * m + (1 - beta1) * g_t
        m = self.get_slot(var, "m")
        m_scaled_g_values = grad * (1 - beta1_t)
        m_t = state_ops.assign(m, m * beta1_t,
                               use_locking=self._use_locking)
        with ops.control_dependencies([m_t]):
            m_t = scatter_add(m, indices, m_scaled_g_values)
        # v_t = beta2 * v + (1 - beta2) * (g_t * g_t)
        v = self.get_slot(var, "v")
        v_scaled_g_values = (grad * grad) * (1 - beta2_t)
        v_t = state_ops.assign(v, v * beta2_t, use_locking=self._use_locking)
        with ops.control_dependencies([v_t]):
            v_t = scatter_add(v, indices, v_scaled_g_values)
        gather_m_t = array_ops.gather(m_t, indices)
        gather_v_t = array_ops.gather(v_t, indices)
        gather_v_sqrt = math_ops.sqrt(gather_v_t)
        var_update = scatter_add(var, indices, -lr * gather_m_t / (gather_v_sqrt + epsilon_t))
        return control_flow_ops.group(*[var_update, m_t, v_t])
View Code

Tensorflow 有個叫 LazyAdamOptimizer 的方案,但試下來穩定比這個實現差。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM