cs231n筆記(二) 最優化方法

回顧上一節中,介紹了圖像分類任務中的兩個要點:git

  1.  假設函數。該函數將原始圖像像素映射爲分類評分值。 
  2. 損失函數。該函數根據分類評分和訓練集圖像數據實際分類的一致性,衡量某個具體參數集的質量好壞。

如今介紹第三個要點,也是最後一個關鍵部分:最優化Optimization。最優化是尋找能使得損失函數值最小化的參數 W 的過程,一旦理解了這三個部分是如何相互運做的,咱們將會回到第一個要點,而後將其拓展爲一個遠比線性函數複雜的函數:首先是神經網絡,而後是卷積神經網絡。而損失函數和最優化過程這兩個部分將會保持相對穩定。github

損失函數可視化本節討論的損失函數通常都是定義在高維度的空間中(好比,在 CIFAR-10 中一個線性分類器的權重矩陣大小是 $[10 \times 3073]$ ,就有 30730 個參數),這樣要將其可視化就很困難。然而辦法仍是有的,在 1 個維度或者 2 個維度的方向上對高維空間進行切片,就能獲得一些直觀感覺。例如,隨機生成一個權重矩陣 W ,將其看作向量,該矩陣就與高維空間中的一個點對應。而後沿着某個維度方向前進的同時記錄損失函數值的變化。換句話說,就是生成一個隨機的方向 $W_1$ 而且沿着此方向計算損失值,計算方法是根據不一樣的 $a$ 值來計算 $L(W + aW_1)$ 。這個過程將生成一個圖表,其 $x$ 軸是 $a$ 值,$y$ 軸是損失函數值。一樣的方法還能夠用在兩個維度上,經過改變 $a,b$ 來計算損失值 $L(W + aW_1 + bW_2)$ ,從而給出二維的圖像。在圖像中,$a,b$ 能夠分別用 $x,y$ 軸表示,而損失函數的值能夠用顏色變化表示:

svm1d

上圖左邊與中間爲一個數據且無正則化的多類 SVM 的損失函數的圖示。右邊是 CIFAR-10 中的 100 個數據。: $a$ 值變化在某個維度方向上對應的的損失值變化。中和右:兩個維度方向上的損失值切片圖,藍色部分是低損失值區域,紅色部分是高損失值區域。注意損失函數的分段線性結構。多個樣本的損失值是整體的平均值,因此右邊的碗狀結構是不少的分段線性結構的平均(好比中間這個就是其中之一)。能夠經過數學公式來解釋損失函數的分段線性結構,對於一個單獨的數據,損失函數的計算公式以下:算法

\[L_i = \sum_{j \ne y_i} max (0,w_j^Tx_i –w_{y_i} ^T x_i + 1)\]數組

經過公式可見,每一個樣本的數據損失值是以 W 爲參數的線性函數的總和(零閾值來源於 $max(0,-)$ 函數)。W 的每一行(即 $w_j$ ),有時候它前面是一個正號(好比當它對應錯誤分類的時候),有時候它前面是一個負號(好比當它是是正確分類的時候)。爲進一步闡明,假設有一個簡單的數據集,其中包含有 3 個只有 1 個維度的點,數據集數據點有 3 個類別。那麼完整的無正則化 SVM 的損失值計算以下:網絡

\begin{aligned}
L_0 &= max (0,w_1^Tx_0 -w_0 ^T x_0 + 1) + max (0,w_2^Tx_0 - w_0 ^T x_0 + 1)\\
L_1 &= max (0,w_0^Tx_1 -w_1 ^T x_1 + 1) + max (0,w_2^Tx_1 - w_1 ^T x_1 + 1)\\
L_2 &= max (0,w_0^Tx_2 -w_2 ^T x_2 + 1) + max (0,w_1^Tx_2 - w_2 ^T x_2 + 1)\\
L   &=\frac{1}{3} (L_0  + L_1 + L_2)
\end{aligned}
dom

由於這些例子都是一維的,因此數據 $x_i$ 和權重 $w_j$ 都是數字。觀察 $w_0$ ,能夠看到上面的式子中一些項是 $w_0$ 的線性函數,且每一項都會與 0 比較,取二者的最大值。可做圖以下:ide

svmbowl

從一個維度方向上對數據損失值的展現。 $x$ 軸方向就是一個權重, $y$ 軸就是損失值。數據損失是多個部分組合而成。其中每一個部分要麼是某個權重的獨立部分,要麼是該權重的線性函數與 0 閾值的比較。完整的 SVM 數據損失就是這個形狀的 30730 維版本。函數

須要多說一句的是,SVM 的損失爲一個凸函數,可是一旦咱們將 $f$ 函數擴展到神經網絡,目標函數就就再也不是凸函數了,圖像也不會像上面那樣是個碗狀,而是凹凸不平的複雜地形形狀。學習

須要注意到:因爲 $\max$ 操做,損失函數中存在一些不可導點這些點使得損失函數不可微,由於在這些不可導點,梯度是沒有定義的。可是次梯度(subgradient)依然存在且經常被使用。在本課中,咱們將交換使用 次梯度 梯度 兩個術語。測試

最優化 Optimization損失函數能夠量化某個具體權重集 W 的優略。而最優化的目標就是找到可以最小化損失函數值的 W 。咱們如今就朝着這個目標前進,實現一個可以最優化損失函數的方法。對於有一些經驗的同窗,這節課看起來有點奇怪,由於使用的例子(SVM 損失函數)是一個凸函數問題。可是要記得,最終的目標是不只僅對凸函數作最優化,而是可以最優化一個神經網絡,而對於神經網絡是不能簡單的使用凸函數的最優化技巧的。

策略#1隨機搜索

既然確認參數集 W 的好壞蠻簡單的,那第一個想到的(差勁)方法,就是能夠隨機嘗試不少不一樣的權重,而後看其中哪一個最好。過程以下:

# 假設X_train的每一列都是一個數據樣本(好比3073 x 50000)
# 假設Y_train是數據樣本的類別標籤(好比一個長50000的一維數組)
# 假設函數L對損失函數進行評價

bestloss = float("inf") # Python assigns the highest possible float value
for num in xrange(1000):
  W = np.random.randn(10, 3073) * 0.0001 # generate random parameters
  loss = L(X_train, Y_train, W) # get the loss over the entire training set
  if loss < bestloss: # keep track of the best solution
    bestloss = loss
    bestW = W
  print 'in attempt %d the loss was %f, best %f' % (num, loss, bestloss)

# 輸出:
# in attempt 0 the loss was 9.401632, best 9.401632
# in attempt 1 the loss was 8.959668, best 8.959668
# in attempt 2 the loss was 9.044034, best 8.959668
# in attempt 3 the loss was 9.278948, best 8.959668
# in attempt 4 the loss was 8.857370, best 8.857370
# in attempt 5 the loss was 8.943151, best 8.857370
# in attempt 6 the loss was 8.605604, best 8.605604
# ... (trunctated: continues for 1000 lines)
View Code
在上面的代碼中,咱們嘗試了若干隨機生成的權重矩陣 W ,其中某些的損失值較小,而另外一些的損失值大些。咱們能夠把此次隨機搜索中找到的最好的權重 W 取出,而後去跑測試集:
# 假設X_test尺寸是[3073 x 10000], Y_test尺寸是[10000 x 1]
scores = Wbest.dot(Xte_cols) # 10 x 10000, the class scores for all test examples
# 找到在每列中評分值最大的索引(即預測的分類)
Yte_predict = np.argmax(scores, axis = 0)
# 以及計算準確率
np.mean(Yte_predict == Yte)
# 返回 0.1555
View Code

驗證集上表現最好的權重 W跑測試集的準確率是 15.5% ,而徹底隨機猜的準確率是 10% ,如此看來,這個準確率對於這樣一個不通過大腦的策略來講,還算不錯嘛!

迭代優化固然,咱們確定能作得更好些。核心思路是:雖然找到最優的權重 W 很是困難,甚至是不可能的(尤爲當 W 中存的是整個神經網絡的權重的時候),但若是問題轉化爲:對一個權重矩陣集 W 取優,使其損失值稍微減小。那麼問題的難度就大大下降了。換句話說,咱們的方法從一個隨機的 W 開始,而後對其迭代取優,每次都讓它的損失值變得更小一點。咱們的策略是從隨機權重開始,而後迭代取優,從而得到更低的損失值。一個助於理解的比喻是把你本身想象成一個蒙着眼睛的徒步者,正走在山地地形上,目標是要慢慢走到山底。在 CIFAR-10 的例子中,這山是 30730 維的(由於 W 是 3073x10 )。咱們在山上踩的每一點都對應一個的損失值,該損失值能夠看作該點的海拔高度。

策略#2:隨機本地搜索

第一個策略能夠看作是每走一步都嘗試幾個隨機方向,若是某個方向是向山下的,就向該方向走一步。此次咱們從一個隨機 W 開始,而後生成一個隨機的擾動 $\delta W$ ,只有當 $W + \delta W$ 的損失值變低,咱們纔會更新。這個過程的具體代碼以下:

W = np.random.randn(10, 3073) * 0.001 # 生成隨機初始W
bestloss = float("inf")
for i in xrange(1000):
  step_size = 0.0001
  Wtry = W + np.random.randn(10, 3073) * step_size
  loss = L(Xtr_cols, Ytr, Wtry)
  if loss < bestloss:
    W = Wtry
    bestloss = loss
  print 'iter %d loss is %f' % (i, bestloss)
View Code

使用一樣的數據(1000),這個方法能夠獲得 21.4%的分類準確率。這個比策略一好,可是依然過於浪費計算資源。

策略#3:跟隨梯度

前兩個策略中,咱們是嘗試在權重空間中隨機找到一個方向,沿着該方向能下降損失函數的損失值。其實不須要隨機尋找方向,由於能夠直接計算出最好的方向,這就是從數學上計算出最陡峭的方向。這個方向就是損失函數的梯度(gradient)。在矇眼徒步者的比喻中,這個方法就比如是感覺咱們腳下山體的傾斜程度,而後向着最陡峭的降低方向下山。

在一維函數中,斜率是函數在某一點的瞬時變化率。梯度是函數的斜率的通常化表達,它不是一個值,而是一個向量。在輸入空間中,梯度是各個維度的斜率組成的向量(或者稱爲導數 derivatives )。對一維函數的求導公式以下:

\[\frac{df(x)}{dx} = \lim _{h \rightarrow 0} \frac{f(x + h) – f (x)}{h}\]

當函數有多個參數的時候,咱們稱導數爲偏導數。而梯度就是在每一個維度上偏導數所造成的向量。

梯度的計算計算梯度有兩種方法:一個是數值梯度法,實現相對簡單。另外一個分析梯度法,計算迅速,結果精確,可是實現時容易出錯,且須要使用微分。如今對兩種方法進行介紹:

上節中的公式已經給出數值計算梯度的方法。下面代碼是一個輸入爲函數 $f$ 和向量 $x$ ,經過數值梯度法計算 $f$ 梯度的通用函數,它返回函數 $f$ 在點 $x$ 處的梯度,:

def eval_numerical_gradient(f, x):
  """  
  一個f在x處的數值梯度法的簡單實現
  - f是隻有一個參數的函數
  - x是計算梯度的點
  """ 
  fx = f(x) # 在原點計算函數值
  grad = np.zeros(x.shape)
  h = 0.00001
  # 對x中全部的索引進行迭代
  it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
  while not it.finished:

    # 計算x+h處的函數值
    ix = it.multi_index
    old_value = x[ix]
    x[ix] = old_value + h # 增長h
    fxh = f(x) # 計算f(x + h)
    x[ix] = old_value # 存到前一個值中 (很是重要)

    # 計算偏導數
    grad[ix] = (fxh - fx) / h # 坡度
    it.iternext() # 到下個維度

  return grad
View Code

根據上面的梯度公式,代碼對全部維度進行迭代,在每一個維度上產生一個很小的變化 h ,經過觀察函數值變化,計算函數在該維度上的偏導數。最後,全部的梯度存儲在變量 grad 中。

注意在數學公式中, $h$ 的取值是趨近於0的,然而在實際中,用一個很小的數值(好比例子中的 $1^{-5}$ )就足夠了。在不產生數值計算出錯的理想前提下,你會使用盡量小的 $h$ 。還有,實際中用中心差值公式(centered difference formula)效果較好:

\[ \frac{f(x)}{dx} = \lim _{h \rightarrow 0}  = \frac{f(x+ h ) – f(x - h)}{2h}\]

可使用上面這個公式來計算任意函數在任意點上的梯度。下面計算權重空間中的某些隨機點上,CIFAR-10 損失函數的梯度:

# 要使用上面的代碼咱們須要一個只有一個參數的函數
# (在這裏參數就是權重)因此也包含了X_train和Y_train
def CIFAR10_loss_fun(W):
  return L(X_train, Y_train, W)

W = np.random.rand(10, 3073) * 0.001 # 隨機權重向量
df = eval_numerical_gradient(CIFAR10_loss_fun, W) # 獲得梯度
View Code

梯度告訴咱們損失函數在每一個維度上的斜率,以此來進行更新:

loss_original = CIFAR10_loss_fun(W) # 初始損失值
print 'original loss: %f' % (loss_original, )

# 查看不一樣步長的效果
for step_size_log in [-10, -9, -8, -7, -6, -5,-4,-3,-2,-1]:
  step_size = 10 ** step_size_log
  W_new = W - step_size * df # 權重空間中的新位置
  loss_new = CIFAR10_loss_fun(W_new)
  print 'for step size %f new loss: %f' % (step_size, loss_new)

# 輸出:
# original loss: 2.200718
# for step size 1.000000e-10 new loss: 2.200652
# for step size 1.000000e-09 new loss: 2.200057
# for step size 1.000000e-08 new loss: 2.194116
# for step size 1.000000e-07 new loss: 2.135493
# for step size 1.000000e-06 new loss: 1.647802
# for step size 1.000000e-05 new loss: 2.844355
# for step size 1.000000e-04 new loss: 25.558142
# for step size 1.000000e-03 new loss: 254.086573
# for step size 1.000000e-02 new loss: 2539.370888
# for step size 1.000000e-01 new loss: 25392.214036
View Code

在梯度負方向上更新:在上面的代碼中,爲了計算 W_new,要注意咱們是向着梯度 df 的負方向去更新,這是由於咱們但願損失函數值是在下降而不是升高。

梯度指明瞭函數在哪一個方向是變化率最大的,可是沒有指明在這個方向上應該走多遠。在後續的課程中能夠看到,選擇步長(也叫做學習率)將會是神經網絡訓練中最重要也是最頭痛的超參數設定之一。仍是用矇眼徒步者下山的比喻,這就比如咱們能夠感受到腳朝向的不一樣方向上,地形的傾斜程度不一樣。可是該跨出多長的步長呢?不肯定。若是謹慎地小步走,狀況可能比較穩定可是進展較慢(這就是步長較小的狀況)。相反,若是想盡快下山,那就大步走吧,但結果也不必定盡如人意。在上面的代碼中就能看見反例,在某些點若是步長過大,反而可能越過最低點致使更高的損失值。

stepsize

將步長效果視覺化的圖例。從某個具體的點 W 開始計算梯度(白箭頭方向是負梯度方向),梯度告訴了咱們損失函數降低最陡峭的方向。小步長降低穩定但進度慢,大步長進展快可是風險更大。採起大步長可能致使錯過最優勢,讓損失值上升。步長(後面會稱其爲學習率)將會是咱們在調參中最重要的超參數之一。

數值梯度的效率問題:你可能已經注意到,計算數值梯度的複雜性和參數的量線性相關。在本例中有 30730 個參數,因此損失函數每走一步就須要計算 30731 次損失函數的梯度。現代神經網絡很容易就有上千萬的參數,所以這個問題只會愈加嚴峻。顯然這個策略不適合大規模數據,咱們須要更好的策略。

微分分析計算梯度使用有限差值近似計算梯度比較簡單,但缺點在於終究只是近似(由於咱們對於 $h$ 值是選取了一個很小的數值,但真正的梯度定義中 $h$ 趨向 0 的極限),且耗費計算資源太多。第二個梯度計算方法是利用微分來分析,能獲得計算梯度的公式(不是近似),用公式計算梯度速度很快,惟一很差的就是實現的時候容易出錯。爲了解決這個問題,在實際操做時經常將分析梯度法的結果和數值梯度法的結果做比較,以此來檢查其實現的正確性,這個步驟叫作梯度檢查

用SVM的損失函數在某個數據點上的計算來舉例:

\[L_i = \sum_{j \ne y_i} max(0, w_j^T x_i – w_{y_i}^T x_i + \Delta)\]

能夠對函數進行微分。好比,對 $w_{y_i}$ 進行微分獲得:

\[\nabla_{w_{y_i}} L_i = - \left ( \sum_{j \ne y_i} \mathbb{I}(w_j^Tx_i - w_{y_i}^T x_i + \Delta > 0) \right )x_i\]

其中 $\mathbb{I}$ 是一個示性函數,若是括號中的條件爲真,那麼函數值爲 1 ,若是爲假,則函數值爲 0 。雖然上述公式看起來複雜,但在代碼實現的時候比較簡單:只須要計算沒有知足邊界值的分類的數量(所以對損失函數產生了貢獻),而後乘以 $x_i$ 就是梯度了。注意,這個梯度只是對應正確分類的 W 的行向量的梯度,那些 $j \ne y_i$ 行的梯度是:
\[\nabla_{w_j} L_i = \mathbb{I}(w_j^Tx_i - w_{y_i}^T x_i + \Delta > 0 ) x_i\]

一旦將梯度的公式微分出來,代碼實現公式並用於梯度更新就比較順暢了。

梯度降低

如今能夠計算損失函數的梯度了,程序重複地計算梯度而後對參數進行更新,這一過程稱爲梯度降低,他的普通版本是這樣的:

# 普通的梯度降低

while True:
  weights_grad = evaluate_gradient(loss_fun, data, weights)
  weights += - step_size * weights_grad # 進行梯度更新
View Code

這個簡單的循環在全部的神經網絡核心庫中都有。雖然也有其餘實現最優化的方法(好比 L-BFGS ),可是到目前爲止,梯度降低是對神經網絡的損失函數最優化中最經常使用的方法。課程中,咱們會在它的循環細節增長一些新的東西(好比更新的具體公式),可是核心思想不變,那就是咱們一直跟着梯度走,直到結果再也不變化。

小批量數據梯度降低( Mini-batch gradient descent :在大規模的應用中(好比 ILSVRC 挑戰賽),訓練數據能夠達到百萬級量級。若是像這樣計算整個訓練集,來得到僅僅一個參數的更新就太浪費了。一個經常使用的方法是計算訓練集中的小批量(batches)數據。例如,在目前最高水平的卷積神經網絡中,一個典型的小批量包含 256 個例子,而整個訓練集是多少呢?一百二十萬個。這個小批量數據就用來實現一個參數更新:

# 普通的小批量數據梯度降低

while True:
  data_batch = sample_training_data(data, 256) # 256個數據
  weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
  weights += - step_size * weights_grad # 參數更新
 
View Code

這個方法之因此效果不錯,是由於訓練集中的數據都是相關的。要理解這一點,能夠想象一個極端狀況:在 ILSVRC 中的 120 萬個圖像是 1000 張不一樣圖片的複製(每一個類別 1 張圖片,每張圖片有 1200 張複製)。那麼顯然計算這 1200 張複製圖像的梯度就應該是同樣的。對比 120 萬張圖片的數據損失的均值與只計算 1000 張的子集的數據損失均值時,結果應該是同樣的。實際狀況中,數據集確定不會包含重複圖像,那麼小批量數據的梯度就是對整個數據集梯度的一個近似。所以,在實踐中經過計算小批量數據的梯度能夠實現更快速地收斂,並以此來進行更頻繁的參數更新。

小批量數據策略有個極端狀況,那就是每一個批量中只有 1 個數據樣本,這種策略被稱爲隨機梯度降低(Stochastic Gradient Descent 簡稱 SGD ,有時候也被稱爲在線梯度降低。這種策略在實際狀況中相對少見,由於向量化操做的代碼一次計算 100 個數據 比 100 次計算 1 個數據要高效不少。即便 SGD 在技術上是指每次使用 1 個數據來計算梯度,你仍是會聽到人們使用 SGD 來指代小批量數據梯度降低(或者用 MGD 來指代小批量數據梯度降低,而 BGD 來指代則相對少見)。小批量數據的大小是一個超參數,可是通常並不須要經過交叉驗證來調參。它通常由存儲器的限制來決定的,或者乾脆設置爲一樣大小,好比 32,64,128 等。之因此使用 2 的指數,是由於在實際中許多向量化操做實現的時候,若是輸入數據量是 2 的倍數,那麼運算更快。

dataflow

信息流的總結圖例。數據集中的 $(x_i,y_i)$ 是給定的。權重從一個隨機數字開始,且能夠改變。在前向傳播時,評分函數計算出類別的得分並存儲在向量 $f$ 中。損失函數包含兩個部分:數據損失和正則化損失。其中,數據損失計算的是分類評分 $f$ 和實際標籤 $y$ 之間的差別,正則化損失只是一個關於權重的函數。在梯度降低過程當中,咱們計算權重的梯度(若是願意的話,也能夠計算數據上的梯度),而後使用它們來實現參數的更新。

在本節中:

  • 將損失函數比做了一個高維度的最優化地形,並嘗試到達它的最底部。最優化的工做過程能夠看作一個蒙着眼睛的徒步者但願摸索着走到山的底部。在例子中,可見 SVM 的損失函數是分段線性的,而且是碗狀的。

  • 提出了迭代優化的思想,從一個隨機的權重開始,而後一步步地讓損失值變小,直到最小。

  • 函數的梯度給出了該函數最陡峭的上升方向。介紹了利用有限的差值來近似計算梯度的方法,該方法實現簡單可是效率較低(有限差值就是 $h$,用來計算數值梯度)。

  • 參數更新須要有技巧地設置步長。也叫學習率。若是步長過小,進度穩定可是緩慢,若是步長太大,進度快可是可能有風險。

  • 討論權衡了數值梯度法和分析梯度法。數值梯度法計算簡單,但結果只是近似且耗費計算資源。分析梯度法計算準確迅速可是實現容易出錯,並且須要對梯度公式進行推導的數學基本功。所以,在實際中使用分析梯度法,而後使用梯度檢查來檢查其實現正確與否,其本質就是將分析梯度法的結果與數值梯度法的計算結果對比。

  • 介紹了梯度降低算法,它在循環中迭代地計算梯度並更新參數。

參考:

https://zhuanlan.zhihu.com/p/21360434

http://cs231n.github.io/optimization-1/

相關文章
相關標籤/搜索