본문 바로가기
Coursera 강의/Deep Learning

[실습] Gradient Checking

by 별준 2020. 9. 26.
해당 내용은 Coursera의 딥러닝 특화과정(Deep Learning Specialization)의 두 번째 강의 
Improving Deep Neural Networks: Hyperparameter tuning, Regularization and Optimization를 듣고 정리한 내용입니다. (Week 1)

1주차 마지막 실습은 Gradient Checking 입니다.

 

사용되는 패키지는 다음과 같습니다.

# Packages
import numpy as np
from testCases import *
from gc_utils import sigmoid, relu, dictionary_to_vector, vector_to_dictionary, gradients_to_vector

Gradient Checking은 BP가 잘 동작하는 지 확인하는 방법 중의 하나 입니다. BP의 과정은 꽤 복잡하고 bug가 있을 수 있는데, Gradient Checking을 통해서 어느정도 확인이 가능합니다.

1. How does gradient checking work ?

BP는 gradient \(\frac{\partial J}{\partial \theta}\)를 계산합니다. \(\theta\)는 모델의 파라미터고, J는 FP와 Cost Function을 통해서 구한 Cost입니다. 

FP는 비교적 수행하기 쉬운 과정이고, 우리는 cost J를 정확하게 구할 수 있고, 우리는 J를 계산하는 코드를 사용해서 \(\frac{\partial J}{\partial \theta}\) 계산을 위한 코드를 증명할 수 있습니다.

도함수(Gradient)의 정의는 다음과 같습니다.

여기서 \(\epsilon \rightarrow 0\)은 \(\epsilon\)값이 매우 작다는 것을 의미하고 실제로 꽤 작은 값으로 Gradient 근사치를 구할 것입니다.

즉, \(\frac{\partial J}{\partial \theta}\)는 정확하게 계산된 gradient이고, 우리는 위 식을 통해서 근사치를 구하고, 비교해서 BP가 정상적으로 동작했는지 확인합니다.

 

2. 1-dimensional gradient checking

1차원 linear function\(J(\theta) = \theta x\)가 있을 때, 이 모델은 single-valued parameter \(\theta\)와 input \(x\)로 이루어져있다. 우리는 J와 \(\frac{\partial J}{\partial \theta}\)을 계산하고, gradient checking을 통해서 구한 gradient가 맞는지 확인하면 됩니다.

# GRADED FUNCTION: forward_propagation

def forward_propagation(x, theta):
    """
    Implement the linear forward propagation (compute J) presented in Figure 1 (J(theta) = theta * x)
    
    Arguments:
    x -- a real-valued input
    theta -- our parameter, a real number as well
    
    Returns:
    J -- the value of function J, computed using the formula J(theta) = theta * x
    """
    
    ### START CODE HERE ### (approx. 1 line)
    J = theta * x
    ### END CODE HERE ###
    
    return J

J의 도함수는 \(\frac{\partial J}{\partial \theta} = x\) 입니다.

# GRADED FUNCTION: backward_propagation

def backward_propagation(x, theta):
    """
    Computes the derivative of J with respect to theta (see Figure 1).
    
    Arguments:
    x -- a real-valued input
    theta -- our parameter, a real number as well
    
    Returns:
    dtheta -- the gradient of the cost with respect to theta
    """
    
    ### START CODE HERE ### (approx. 1 line)
    dtheta = x
    ### END CODE HERE ###
    
    return dtheta

 

다음으로 gradient의 근사치를 구합니다.

위 식을 통해서 구하고, 꽤 작은 \(\epsilon\)값을 사용합니다. 

위 과정을 통해서 gradient의 근사치를 구하고, BP를 통해서 gradient를 구합니다.

그리고 gradient와 gradient 근사치의 relative difference를 다음의 식을 통해서 구합니다.

위 식은 다음과 같은 단계를 통해서 계산합니다.

1. Compute the numerator using np.linalg.norm(...)

2. Compute the denominator using np.linalg.norm(...) twice

3. divide them

만약 difference가 작다면(약 \(10^{-7}\) 이하), BP를 통해 계산한 Gradient가 맞다고 예상할 수 있습니다. 그렇지 않다면, gradient 계산에 실수가 있다는 것입니다.

# GRADED FUNCTION: gradient_check

def gradient_check(x, theta, epsilon = 1e-7):
    """
    Implement the backward propagation presented in Figure 1.
    
    Arguments:
    x -- a real-valued input
    theta -- our parameter, a real number as well
    epsilon -- tiny shift to the input to compute approximated gradient with formula(1)
    
    Returns:
    difference -- difference (2) between the approximated gradient and the backward propagation gradient
    """
    
    # Compute gradapprox using left side of formula (1). epsilon is small enough, you don't need to worry about the limit.
    ### START CODE HERE ### (approx. 5 lines)
    thetaplus = theta + epsilon                               # Step 1
    thetaminus = theta - epsilon                           # Step 2
    J_plus = x * thetaplus                                  # Step 3
    J_minus = x * thetaminus                                 # Step 4
    gradapprox = (J_plus - J_minus) / (2*epsilon)                              # Step 5
    ### END CODE HERE ###
    
    # Check if gradapprox is close enough to the output of backward_propagation()
    ### START CODE HERE ### (approx. 1 line)
    grad = x
    ### END CODE HERE ###
    
    ### START CODE HERE ### (approx. 1 line)
    numerator = np.linalg.norm(grad - gradapprox)                               # Step 1'
    denominator = np.linalg.norm(grad) + np.linalg.norm(gradapprox)                             # Step 2'
    difference = numerator / denominator                              # Step 3'
    ### END CODE HERE ###
    
    if difference < 1e-7:
        print ("The gradient is correct!")
    else:
        print ("The gradient is wrong!")
    
    return difference

 

3. N-dimensional gradient checking

 

3-layer NN은 다음과 같은 과정을 통해서 FP와 BP가 진행됩니다.

 

FP와 BP 코드는 다음과 같다.

def forward_propagation_n(X, Y, parameters):
    """
    Implements the forward propagation (and computes the cost) presented in Figure 3.
    
    Arguments:
    X -- training set for m examples
    Y -- labels for m examples 
    parameters -- python dictionary containing your parameters "W1", "b1", "W2", "b2", "W3", "b3":
                    W1 -- weight matrix of shape (5, 4)
                    b1 -- bias vector of shape (5, 1)
                    W2 -- weight matrix of shape (3, 5)
                    b2 -- bias vector of shape (3, 1)
                    W3 -- weight matrix of shape (1, 3)
                    b3 -- bias vector of shape (1, 1)
    
    Returns:
    cost -- the cost function (logistic cost for one example)
    """
    
    # retrieve parameters
    m = X.shape[1]
    W1 = parameters["W1"]
    b1 = parameters["b1"]
    W2 = parameters["W2"]
    b2 = parameters["b2"]
    W3 = parameters["W3"]
    b3 = parameters["b3"]

    # LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SIGMOID
    Z1 = np.dot(W1, X) + b1
    A1 = relu(Z1)
    Z2 = np.dot(W2, A1) + b2
    A2 = relu(Z2)
    Z3 = np.dot(W3, A2) + b3
    A3 = sigmoid(Z3)

    # Cost
    logprobs = np.multiply(-np.log(A3),Y) + np.multiply(-np.log(1 - A3), 1 - Y)
    cost = 1./m * np.sum(logprobs)
    
    cache = (Z1, A1, W1, b1, Z2, A2, W2, b2, Z3, A3, W3, b3)
    
    return cost, cache
def backward_propagation_n(X, Y, cache):
    """
    Implement the backward propagation presented in figure 2.
    
    Arguments:
    X -- input datapoint, of shape (input size, 1)
    Y -- true "label"
    cache -- cache output from forward_propagation_n()
    
    Returns:
    gradients -- A dictionary with the gradients of the cost with respect to each parameter, activation and pre-activation variables.
    """
    
    m = X.shape[1]
    (Z1, A1, W1, b1, Z2, A2, W2, b2, Z3, A3, W3, b3) = cache
    
    dZ3 = A3 - Y
    dW3 = 1./m * np.dot(dZ3, A2.T)
    db3 = 1./m * np.sum(dZ3, axis=1, keepdims = True)
    
    dA2 = np.dot(W3.T, dZ3)
    dZ2 = np.multiply(dA2, np.int64(A2 > 0))
    dW2 = 1./m * np.dot(dZ2, A1.T) * 2
    db2 = 1./m * np.sum(dZ2, axis=1, keepdims = True)
    
    dA1 = np.dot(W2.T, dZ2)
    dZ1 = np.multiply(dA1, np.int64(A1 > 0))
    dW1 = 1./m * np.dot(dZ1, X.T)
    db1 = 4./m * np.sum(dZ1, axis=1, keepdims = True)
    
    gradients = {"dZ3": dZ3, "dW3": dW3, "db3": db3,
                 "dA2": dA2, "dZ2": dZ2, "dW2": dW2, "db2": db2,
                 "dA1": dA1, "dZ1": dZ1, "dW1": dW1, "db1": db1}
    
    return gradients

How does gradient checking work?

위 1, 2의 과정과 같이 똑같이 gradient의 근사치를 아래 공식을 통해서 계산합니다. 

그리고 계산한 gradient 근사치를 BP를 통해 구한 gradient와 비교합니다.

파라미터가 더이상 scalar가 아닌 matrix이고, 파라미터들을 1차원 벡터로 변환해 values 변수에 저장해서 각 layer의 gradient 근사치를 구합니다.

gradient check 함수는 다음과 과정을 통해 진행됩니다.

위 과정을 통해서 우리는 gradapprox vector를 구할 수 있고, BP를 통해 구한 grads와 비교하여 difference를 구합니다.

# GRADED FUNCTION: gradient_check_n

def gradient_check_n(parameters, gradients, X, Y, epsilon = 1e-7):
    """
    Checks if backward_propagation_n computes correctly the gradient of the cost output by forward_propagation_n
    
    Arguments:
    parameters -- python dictionary containing your parameters "W1", "b1", "W2", "b2", "W3", "b3":
    grad -- output of backward_propagation_n, contains gradients of the cost with respect to the parameters. 
    x -- input datapoint, of shape (input size, 1)
    y -- true "label"
    epsilon -- tiny shift to the input to compute approximated gradient with formula(1)
    
    Returns:
    difference -- difference (2) between the approximated gradient and the backward propagation gradient
    """
    
    # Set-up variables
    parameters_values, _ = dictionary_to_vector(parameters)
    grad = gradients_to_vector(gradients)
    num_parameters = parameters_values.shape[0]
    J_plus = np.zeros((num_parameters, 1))
    J_minus = np.zeros((num_parameters, 1))
    gradapprox = np.zeros((num_parameters, 1))
    
    # Compute gradapprox
    for i in range(num_parameters):
        
        # Compute J_plus[i]. Inputs: "parameters_values, epsilon". Output = "J_plus[i]".
        # "_" is used because the function you have to outputs two parameters but we only care about the first one
        ### START CODE HERE ### (approx. 3 lines)
        thetaplus = np.copy(parameters_values)                                      # Step 1
        thetaplus[i][0] = thetaplus[i][0] + epsilon                                # Step 2
        J_plus[i], _ = forward_propagation_n(X, Y, vector_to_dictionary(thetaplus))                                   # Step 3
        ### END CODE HERE ###
        
        # Compute J_minus[i]. Inputs: "parameters_values, epsilon". Output = "J_minus[i]".
        ### START CODE HERE ### (approx. 3 lines)
        thetaminus = np.copy(parameters_values)                                     # Step 1
        thetaminus[i][0] = thetaminus[i][0] - epsilon                               # Step 2        
        J_minus[i], _ = forward_propagation_n(X, Y, vector_to_dictionary(thetaminus))                                  # Step 3
        ### END CODE HERE ###
        
        # Compute gradapprox[i]
        ### START CODE HERE ### (approx. 1 line)
        gradapprox[i] = (J_plus[i] - J_minus[i]) / (2*epsilon)
        ### END CODE HERE ###
    
    # Compare gradapprox to backward propagation gradients by computing difference.
    ### START CODE HERE ### (approx. 1 line)
    numerator = np.linalg.norm(grad - gradapprox)                                           # Step 1'
    denominator = np.linalg.norm(grad) + np.linalg.norm(gradapprox)                                         # Step 2'
    difference = numerator / denominator                                          # Step 3'
    ### END CODE HERE ###

    if difference > 2e-7:
        print ("\033[93m" + "There is a mistake in the backward propagation! difference = " + str(difference) + "\033[0m")
    else:
        print ("\033[92m" + "Your backward propagation works perfectly fine! difference = " + str(difference) + "\033[0m")
    
    return difference

 

주의할 점은 Gradient Checking은 매우 느리다는 것입니다. gradient의 근사치를 구하는 작업이 계산 연산이 크기 때문입니다. 이러한 이유로 매 반복마다 Gradient Checking을 진행하지는 않으며, gradient가 올바른지 확인하기 위해서 몇 번만 수행합니다. 

또한, Gradient Checking은 dropout에서 동작하지 않습니다. 그래서 일반적으로는 Gradient Checking을 진행하고, dropout을 적용합니다.

 

What you should remember from this notebook:

  • Gradient checking verifies closeness between the gradients from backpropagation and the numerical approximation of the gradient (computed using forward propagation).
  • Gradient checking is slow, so we don't run it in every iteration of training. You would usually run it only to make sure your code is correct, then turn it off and use backprop for the actual learning process.

지금까지 BP가 정상적으로 잘 동작하는 지 확인하는 기법인 Gradient Checking에 대해서 알아보았습니다.

댓글