本文介紹瞭如何使用梯度檢驗方法確認反向傳播程式碼是否準確。
在《Coding Neural Network - Forward Propagation and Backpropagation》一文中,我們藉助 numpy 實現了前向傳播和反向傳播演算法。但從頭開始實現反向傳播很容易遇到 bug 或者報錯。因此,在訓練資料上執行神經網路之前,必須檢驗反向傳播的實現是否正確。不過首先,我們先複習一下反向傳播的概念:從最後的節點開始,沿著拓撲排序的反方向遍歷所有節點,計算每個邊的尾節點相對於損失函式的導數。換言之,計算損失函式對所有引數的導數:∂J/∂θ,其中θ表示模型中的引數。
我們通過計算數值梯度並比較數值梯度和根據反向傳播求出的梯度(解析梯度)間的差異,來測試我們的實現程式碼。這裡有兩種數值梯度的計算方法:
右邊形式:
[J(θ+ϵ)−J(θ)]/ϵ
雙邊形式(見圖 2):
[J(θ+ϵ)−J(θ−ϵ)]/2ϵ
圖 2:雙邊數值梯度
逼近導數的雙邊形式比右邊形式更接近真實值。我們以 f(x)=x^2 為例,在 x=3 處計算導數。
解析導數:∇_xf(x)=2x ⇒∇_xf(3)=6
雙邊數值導數:[(3+1e−2)^2−(3−1e−2)^2]/[2∗1e−2]=5.999999999999872
右邊數值導數:[(3+1e−2)^2−3^2]/[1e−2]=6.009999999999849
可以看到,解析梯度和雙邊數值梯度之間的差值幾乎為零;而和右邊形式的數值梯度之間的差值為 0.01。因此在下文中,我們使用雙邊形式計算數值梯度。
另外,我們使用下式對數值梯度和解析梯度間的差值進行標準化。
如果差值≤10^−7,可以認為反向傳播的實現程式碼沒有問題;否則,就需要回去檢查程式碼,因為一定有什麼地方出錯了。
以下是完成梯度檢驗的步驟:
1. 隨機從訓練集中抽取一些樣本,用來計算數值梯度和解析梯度(不要使用所有訓練樣本,因為梯度檢驗執行會很慢)。
2. 初始化引數。
3. 計算前向傳播和交叉熵損失。
4. 利用寫好的反向傳播的實現程式碼計算梯度(解析梯度)。
5. 計算雙邊形式的數值梯度。
6. 計算數值梯度和解析解梯度的差值。
這裡,我們使用《Coding Neural Network - Forward Propagation and Backpropagation》中所寫的函式來實現引數初始化、前向傳播、反向傳播以及交叉熵損失的計算。
匯入資料。
# Loading packages
import sys
import h5py
import matplotlib.pyplot as plt
import numpy as np
from numpy.linalg import norm
import seaborn as sns
sys.path.append("../scripts/")
from coding_neural_network_from_scratch import (initialize_parameters,
L_model_forward,
L_model_backward,
compute_cost)
# Import the data
train_dataset = h5py.File("../data/train_catvnoncat.h5")
X_train = np.array(train_dataset["train_set_x"]).T
y_train = np.array(train_dataset["train_set_y"]).T
X_train = X_train.reshape(-1, 209)
y_train = y_train.reshape(-1, 209)
X_train.shape, y_train.shape
((12288, 209), (1, 209))
編寫 helper 函式,幫助實現引數和梯度詞典(gradients dictionary)到向量的相互轉換。
def dictionary_to_vector(params_dict):"""
Roll a dictionary into a single vector.
Arguments
---------
params_dict : dict
learned parameters.
Returns
-------
params_vector : array
vector of all parameters concatenated.
"""count = 0for key in params_dict.keys():new_vector = np.reshape(params_dict[key], (-1, 1))if count == 0:theta_vector = new_vectorelse:theta_vector = np.concatenate((theta_vector, new_vector))count += 1return theta_vectordef vector_to_dictionary(vector, layers_dims):"""
Unroll parameters vector to dictionary using layers dimensions.
Arguments
---------
vector : array
parameters vector.
layers_dims : list or array_like
dimensions of each layer in the network.
Returns
-------
parameters : dict
dictionary storing all parameters.
"""L = len(layers_dims)parameters = {}k = 0for l in range(1, L):# Create temp variable to store dimension used on each layerw_dim = layers_dims[l] * layers_dims[l - 1]b_dim = layers_dims[l]# Create temp var to be used in slicing parameters vectortemp_dim = k + w_dim# add parameters to the dictionaryparameters["W" + str(l)] = vector[k:temp_dim].reshape(layers_dims[l], layers_dims[l - 1])parameters["b" + str(l)] = vector[temp_dim:temp_dim + b_dim].reshape(b_dim, 1)k += w_dim + b_dimreturn parametersdef gradients_to_vector(gradients):"""
Roll all gradients into a single vector containing only dW and db.
Arguments
---------
gradients : dict
storing gradients of weights and biases for all layers: dA, dW, db.
Returns
-------
new_grads : array
vector of only dW and db gradients.
"""# Get the number of indices for the gradients to iterate overvalid_grads = [key for key in gradients.keys()if not key.startswith("dA")]L = len(valid_grads)// 2count = 0# Iterate over all gradients and append them to new_grads listfor l in range(1, L + 1):if count == 0:new_grads = gradients["dW" + str(l)].reshape(-1, 1)new_grads = np.concatenate((new_grads, gradients["db" + str(l)].reshape(-1, 1)))else:new_grads = np.concatenate((new_grads, gradients["dW" + str(l)].reshape(-1, 1)))new_grads = np.concatenate((new_grads, gradients["db" + str(l)].reshape(-1, 1)))count += 1return new_grads
最後,編寫梯度檢驗函式,利用此函式計算解析梯度和數值梯度之間的差值,並藉此判斷反向傳播的實現程式碼是否正確。我們隨機抽取 1 個樣本來計算差值:
def forward_prop_cost(X, parameters, Y, hidden_layers_activation_fn="tanh"):"""
Implements the forward propagation and computes the cost.
Arguments
---------
X : 2d-array
input data, shape: number of features x number of examples.
parameters : dict
parameters to use in forward prop.
Y : array
true "label", shape: 1 x number of examples.
hidden_layers_activation_fn : str
activation function to be used on hidden layers: "tanh", "relu".
Returns
-------
cost : float
cross-entropy cost.
"""# Compute forward propAL, _ = L_model_forward(X, parameters, hidden_layers_activation_fn)# Compute costcost = compute_cost(AL, Y)return costdef gradient_check(parameters, gradients, X, Y, layers_dims, epsilon=1e-7,hidden_layers_activation_fn="tanh"):"""
Checks if back_prop computes correctly the gradient of the cost output by
forward_prop.
Arguments
---------
parameters : dict
storing all parameters to use in forward prop.
gradients : dict
gradients of weights and biases for all layers: dA, dW, db.
X : 2d-array
input data, shape: number of features x number of examples.
Y : array
true "label", shape: 1 x number of examples.
epsilon :
tiny shift to the input to compute approximate gradient.
layers_dims : list or array_like
dimensions of each layer in the network.
Returns
-------
difference : float
difference between approx gradient and back_prop gradient
"""# Roll out parameters and gradients dictionariesparameters_vector = dictionary_to_vector(parameters)gradients_vector = gradients_to_vector(gradients)# Create vector of zeros to be used with epsilongrads_approx = np.zeros_like(parameters_vector)for i in range(len(parameters_vector)):# Compute cost of theta + epsilontheta_plus = np.copy(parameters_vector)theta_plus[i] = theta_plus[i] + epsilonj_plus = forward_prop_cost(X, vector_to_dictionary(theta_plus, layers_dims), Y,hidden_layers_activation_fn)# Compute cost of theta - epsilontheta_minus = np.copy(parameters_vector)theta_minus[i] = theta_minus[i] - epsilonj_minus = forward_prop_cost(X, vector_to_dictionary(theta_minus, layers_dims), Y,hidden_layers_activation_fn)# Compute numerical gradientsgrads_approx[i] = (j_plus - j_minus) / (2 * epsilon)# Compute the difference of numerical and analytical gradientsnumerator = norm(gradients_vector - grads_approx)denominator = norm(grads_approx) + norm(gradients_vector)difference = numerator / denominatorif difference > 10e-7:print ("\033[31mThere is a mistake in back-propagation " +\
"implementation. The difference is: {}".format(difference))else:print ("\033[32mThere implementation of back-propagation is fine! "+\
"The difference is: {}".format(difference))return difference
# Set up neural network architecture
layers_dims = [X_train.shape[0], 5, 5, 1]
# Initialize parameters
parameters = initialize_parameters(layers_dims)
# Randomly selecting 1 example from training data
perms = np.random.permutation(X_train.shape[1])
index = perms[:1]
# Compute forward propagation
AL, caches = L_model_forward(X_train[:, index], parameters, "tanh")
# Compute analytical gradients
gradients = L_model_backward(AL, y_train[:, index], caches, "tanh")
# Compute difference of numerical and analytical gradients
difference = gradient_check(parameters, gradients, X_train[:, index], y_train[:, index], layers_dims)
反向傳播的實現是 OK 的!這裡的差值是 3.0220555297630148e-09
結論
以下是一些關鍵點:
雙邊形式的數值梯度在逼近解析梯度時效果比單邊形式的數值梯度更好。
由於梯度檢驗的執行很慢,因此:
進行梯度檢驗時,只使用一個或少數樣本;
在確認反向傳播的實現程式碼無誤後,訓練神經網路時記得取消梯度檢驗函式的呼叫。
如果使用了 drop-out 策略,(直接進行)梯度檢驗會失效。可以在進行梯度檢驗時,將 keep-prob 設定為 1,訓練神經網路時,再進行修改。
通常採用 e=10e-7 作為檢查解析梯度和數值梯度間差值的基準。如果差值小於 10e-7,則反向傳播的實現程式碼沒有問題。
幸運的是,在諸如 TensorFlow、PyTorch 等深度學習框架中,我們幾乎不需要自己實現反向傳播,因為這些框架已經幫我們計算好梯度了;但是,在成為一個深度學習工作者之前,動手實現這些演算法是很好的練習,可以幫助我們理解其中的原理。
原始碼地址:https://github.com/ImadDabbura/blog-posts/blob/master/notebooks/Coding-Neural-Network-Gradient-Checking.ipynb