Andrew NG 深度學習課程筆記:梯度下降與向量化操作從屬於筆者的Deep Learning Specialization 課程筆記系列文章,本文主要記述了筆者學習 Andrew NG Deep Learning Specialization 系列課程的筆記與程式碼實現。注意,本篇有大量的數學符號與表示式,部分網頁並不支援;可以前往原始檔檢視較好的排版或者在自己的編輯器中開啟。
梯度下降與向量化操作
我們在前文二元分類與 Logistic 迴歸中建立了 Logistic 迴歸的預測公式:
$$
hat{y} = sigma(w^Tx + b), , sigma(z) = frac{1}{1+e^{-z}}
$$
整個訓練集的損失函式為:
$$
J(w,b) =
frac{1}{m}sum{i=1}^mL(hat{y}^{(i)} – y^{(i)}) =
-frac{1}{m} sum{i=1}^m [y^{(i)}loghat{y}^{(i)} + (1-y^{(i)})log(1-hat{y}^{(i)})]
$$
模型的訓練目標即是尋找合適的 $w$ 與 $b$ 以最小化代價函式值;簡單起見我們先假設 $w$ 與 $b$ 都是一維實數,那麼可以得到如下的 $J$ 關於 $w$ 與 $b$ 的圖:
上圖所示的函式 $J$ 即是典型的凸函式,與非凸函式的區別在於其不含有多個區域性最低點;選擇這樣的代價函式就保證了無論我們初始化模型引數如何,都能夠尋找到合適的最優解。如果我們僅考慮對於 $w$ 引數進行更新,則可以得到如下的一維圖形:
引數 $w$ 的更新公式為:
$$
w := w – alpha frac{dJ(w)}{dw}
$$
其中 $alpha$ 表示學習速率,即每次更新的 $w$ 的步伐長度;當 $w$ 大於最優解 $w`$ 時,導數大於0;即 $frac{dJ(w)}{dw}$ 的值大於 0,那麼 $w$ 就會向更小的方向更新。反之當 $w$ 小於最優解 $w`$ 時,導數小於 0,那麼 $w$ 就會向更大的方向更新。
導數
本部分是對於微積分中導數(Derivative)相關理論進行簡單講解,熟悉的同學可以跳過。
上圖中,$a = 2$ 時,$f(a) = 6$;$a = 2.001$ 時,$f(a) = 6.003$,導數為 $frac{6.003 – 6}{2.001 – 2} = 3$;在某個直線型函式中,其導數值是恆定不變的。我們繼續考慮下述二次函式:
上圖中,$a = 2$ 時,$f(a) = 4$;$a = 2.001$ 時,$f(a) approx 4.004$,此處的導數即為 4。而當 $a = 5$ 時,此處的導數為 10;可以發現二次函式的導數值隨著 $x$ 值的變化而變化。下表列舉出了常見的導數:
下表列舉了常用的導數複合運算公式:
計算圖(Computation Graph)
神經網路中的計算即是由多個計算網路輸出的前向傳播與計算梯度的後向傳播構成,我們可以將複雜的代價計算函式切割為多個子過程:
$$
J(a, b, c) = 3 imes (a + bc)
$$
定義 $u = bc$ 以及 $v = a + u$ 和 $J = 3v$,那麼整個計算圖可以定義如下:
根據導數計算公式,我們可知:
$$
frac{dJ}{dv} = 3, ,
frac{dJ}{da} = frac{dJ}{dv} frac{dv}{da} = 3
$$
在複雜的計算圖中,我們往往需要經過大量的中間計算才能得到最終輸出值與原始引數的導數 $dvar = frac{dFinalOutputVar}{d{var}`}$,這裡的 $dvar$ 即表示了最終輸出值相對於任意中間變數的導數。而所謂的反向傳播(Back Propagation)即是當我們需要計算最終值相對於某個特徵變數的導數時,我們需要利用計算圖中上一步的結點定義。
Logistic 迴歸中的導數計算
我們在上文中討論過 Logistic 迴歸的損失函式計算公式為:
$$
z = w^Tx + b
hat{y} = a = sigma(z)
L(a,y) = -( ylog(a) + (1-y)log(1-a) )
$$
這裡我們假設輸入的特徵向量維度為 2,即輸入引數共有 $x_1, w_1, x_2, w_2, b$ 這五個;可以推匯出如下的計算圖:
首先我們反向求出 $L$ 對於 $a$ 的導數:
$$
da = frac{dL(a,y)}{da} = -frac{y}{a} + frac{1-y}{1-a}
$$
然後繼續反向求出 $L$ 對於 $z$ 的導數:
$$
dz = frac{dL}{dz}
=frac{dL(a,y)}{dz}
= frac{dL}{da} frac{da}{dz}
= a-y
$$
依次類推求出最終的損失函式相較於原始引數的導數之後,根據如下公式進行引數更新:
$$
w_1 := w_1 – alpha dw_1
w_2 := w_2 – alpha dw2
b := b – alpha db
$$
接下來我們需要將對於單個用例的損失函式擴充套件到整個訓練集的代價函式:
$$
J(w,b) = frac{1}{m} sum{i=1}^m L(a^{(i)},y)
a^{(i)} = hat{y}^{(i)} = sigma(z^{(i)}) = sigma(w^Tx^{(i)} + b)
$$
我們可以對於某個權重引數 $w_1$,其導數計算為:
$$
frac{partial J(w,b)}{partial w1} = frac{1}{m} sum{i=1}^m frac{partial}{partial w_1}L(a^{(i)},y^{(i)})
$$
完整的 Logistic 迴歸中某次訓練的流程如下,這裡僅假設特徵向量的維度為 2:
向量化操作
在上述的 $m$ 個訓練用例的 Logistic 迴歸中,每次訓練我們需要進行兩層迴圈,外層迴圈遍歷所有的特徵,內層迴圈遍歷所有的訓練用例;如果特徵向量的維度或者訓練用例非常多時,多層迴圈無疑會大大降低執行效率,因此我們使用向量化(Vectorization)操作來進行實際的訓練。我們首先來討論下何謂向量化操作。在 Logistic 迴歸中,我們需要計算 $z = w^Tx + b$,如果是非向量化的迴圈方式操作,我們可能會寫出如下的程式碼:
z = 0;
for i in range(n_x):
z += w[i] * x[i]
z += b複製程式碼
而如果是向量化的操作,我們的程式碼則會簡潔很多:
z = np.dot(w, x) + b複製程式碼
在未來的章節中我們會實際比較迴圈操作與向量化操作二者的效能差異,可以發現向量化操作能夠帶來近百倍的效能提升;目前無論 GPU 還是 CPU 環境都內建了並行指令集, SIMD(Single Instruction Multiple Data),因此無論何時我們都應該儘可能避免使用顯式的迴圈。Numpy 還為我們提供了很多便捷的向量轉化操作,譬如 np.exp(v)
用於進行指數計算,np.log(v)
用於進行對數計算,np.abs(v)
用於進行絕對值計算。
下面我們將上述的 Logistic 迴歸流程轉化為向量化操作,其中輸入資料可以變為 $n_x imes m$ 的矩陣,即共有 $m$ 個訓練用例,每個用例的維度為 $nx$:
$$
Z = np.dot(W^TX) + b
A = [a^{(1)},a^{(2)},…,a^{(m)}] = sigma(z)
$$
我們可以得到各個變數梯度計算公式為:
$$
dZ = A – Y = [a^{(1)} y^{(1)}…]
db = frac{1}{m}sum{i=1}^mdz^{(i)}=frac{1}{m}np.sum(dZ)
dW = frac{1}{m} X dZ^{T}=
frac{1}{m}
egin{bmatrix}
vdots
x^{(i1)} … x^{(im)}
vdots
end{bmatrix}
egin{bmatrix}
vdots
dz^{(i)}
vdots
end{bmatrix}
= frac{1}{m}
egin{bmatrix}
vdots
x^{(1)}dz^{(1)} + … + x^{(m)}dz^{(m)}
vdots
end{bmatrix}
$$