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

[실습] Planar data classification with a hidden layer

by 별준 2020. 9. 25.
해당 내용은 Coursera의 딥러닝 특화과정(Deep Learning Specialization)의 첫 번째 강의 Neural Networks and Deep Learning를 듣고 정리한 내용입니다. (Week 3)

3주차에서는 Planar data 분류기를 구현하는데, 1개의 hidden layer를 가진 neural network를 구현할 것입니다.

이 실습을 통해서 다음의 내용을 확인할 수 있습니다.

  • Implement a 2-class classification neural network with a single hidden layer
  • Use units with a non-linear activation function, such as tanh
  • Compute the cross entropy loss
  • Implement forward and backward propagation

 

1. Packages

사용되는 라이브러리입니다.

  • numpy is the fundamental package for scientific computing with Python.
  • sklearn provides simple and efficient tools for data mining and data analysis.
  • matplotlib is a library for plotting graphs in Python.
  • testCases provides some test examples to assess the correctness of your functions
  • planar_utils provide various useful functions used in this assignment
# Package imports
import numpy as np
import matplotlib.pyplot as plt
from testCases_v2 import *
import sklearn
import sklearn.datasets
import sklearn.linear_model
from planar_utils import plot_decision_boundary, sigmoid, load_planar_dataset, load_extra_datasets

%matplotlib inline

np.random.seed(1) # set a seed so that the results are consistent

2. Dataset

X와 Y에 데이터를 읽어오고, X의 차원을 확인해봅시다.

읽어온 데이터를 그래프로 나타내봅니다. 이 데이터를 꽃 모양처럼 생겼고, red(label y = 0)와 blue(label y = 1)의 포인트로 이루어져 있습니다. 우리는 이 데이터에서 red와 blue 영역을 나누는 것이 목적입니다.

X와 Y의 차원과 입력 샘플의 개수 m을 구해봅시다.

### START CODE HERE ### (≈ 3 lines of code)
shape_X = X.shape
shape_Y = Y.shape
m = X.shape[1]  # training set size
### END CODE HERE ###

print ('The shape of X is: ' + str(shape_X))
print ('The shape of Y is: ' + str(shape_Y))
print ('I have m = %d training examples!' % (m))

X의 차원은 (2, 400), Y의 차원은 (1, 400)이며, 우리는 샘플의 개수 m이 400이라는 것을 알 수 있습니다. 샘플은 각 포인트가 되는 것이죠.

 

3. Simple Logistic Regression

신경망을 구성하기 전에 간단히 Logistic Regression을 수행해서 구역을 분리해 봅시다. sklearn의 내장된 클래스를 사용해서 수행해보겠습니다.

 

# Train the logistic regression classifier
clf = sklearn.linear_model.LogisticRegressionCV();
clf.fit(X.T, Y.T);

# Plot the decision boundary for logistic regression
plot_decision_boundary(lambda x: clf.predict(x), X, Y)
plt.title("Logistic Regression")

# Print accuracy
LR_predictions = clf.predict(X.T)
print ('Accuracy of logistic regression: %d ' % float((np.dot(Y,LR_predictions) + np.dot(1-Y,1-LR_predictions))/float(Y.size)*100) +
       '% ' + "(percentage of correctly labelled datapoints)")

sklearn의 LogisticRegressionCV 클래스를 사용해서 모델을 학습했습니다.(3번째 줄에서 X와 Y로 학습을 했습니다)

그리고 Logistic Regression으로 인한 Decision Boundary를 나타내면 다음과 같이 나타납니다.

dataset이 선형적으로 분리되지 않기 때문에, Logistic Regression은 잘 동작하지 않습니다. 

NN으로 한 번 시도해봅시다.

 

4. Neural Network model

우리는 1개의 hidden layer를 가진 NN을 학습해볼 것입니다.

위의 모델을 구현할 것이고, 하나의 샘플 \(x^{(i)}\)에 대해서 다음을 구해야 합니다.

\[\tag{1} z^{[1](i)} = W^{[1]}x^{(i)} + b^{[1]}\]

\[\tag{2} a^{[1](i)} = tanh(z^{[1](i)})\]

\[\tag{3} z^{[2](i)} = W^{[2]}x^{(i)} + b^{[2]}\]

\[\tag{4} \hat{y}^{(i)} = a^{[2](i)} = \sigma(z^{[2](i)})\]

\[\tag{5} y^{(i)}_{\text{prediction}} = \left\{\begin{matrix} 1 && \text{if } a^{[2](i)} > 0.5 \\ 0 && \text{otherwise} \end{matrix}\right.\]

Cost Function J는 아래와 같이 계산됩니다.

\[\tag{6} J = -\frac{1}{m}\sum_{i = 0}^{m}(y^{(i)}log(a^{[2](i)}) + (1 - y^{(i)})log(1 - a^{[2](i)}))\]

 

그리고 NN은 다음과 같은 단계로 진행됩니다.

1. Define the neural network structure( # of input units, # of hidden units, etc)

2. 모델의 파라미터 초기화

3. 아래 과정을 반복

  • Forward propagation 수행
  • Compute loss
  • Gradients(미분값)을 얻기 위해 Backward propagation 수행
  • Update parameteres(Gradient descent)

우리는 위 1~3단계를 각각의 함수로 정의하고 마지막에 nn_model 함수로 합칠 것입니다.

 

4.1 Defining the neural network structure

네트워크 구조를 정의하기 위해서 우리는 다음과 같은 값들을 선택합니다.

- n_x : the size of the input layer

- n_h : the size of the hidden layer (set this to 4 : 여기서는 4로 지정-> 4개의 units)

- n_y : the size of output layer

 

아까 data에서 봤듯이 input layer의 크기는 \(x_1, x_2\)로 2가 되며, output layer의 크기는 red or blue로 2개의 클래스로 한 개의 결과(0 or 1)를 갖습니다.

# GRADED FUNCTION: layer_sizes

def layer_sizes(X, Y):
    """
    Arguments:
    X -- input dataset of shape (input size, number of examples)
    Y -- labels of shape (output size, number of examples)
    
    Returns:
    n_x -- the size of the input layer
    n_h -- the size of the hidden layer
    n_y -- the size of the output layer
    """
    ### START CODE HERE ### (≈ 3 lines of code)
    n_x = X.shape[0] # size of input layer
    n_h = 4
    n_y = Y.shape[0] # size of output layer
    ### END CODE HERE ###
    return (n_x, n_h, n_y)

data의 행이 feature의 수이기 때문에 shape[0]을 통해서 feature 크기, 즉, layer의 size를 얻을 수 있습니다.

 

4.2 Initialize the model's parameters

파라미터를 초기화하는데, NN에서 w는 0이 아닌 랜덤한 값으로 초기화를 해야합니다. 그렇지 않으면, NN은 제대로 동작하지 않기 때문입니다. 파라미터 b는 0으로 초기화해도 무관합니다.

다음의 numpy 함수를 사용해서 초기화합니다.

W = np.random.randn(shape) * 0.01

b = np.zeros((shape))

 

# GRADED FUNCTION: initialize_parameters

def initialize_parameters(n_x, n_h, n_y):
    """
    Argument:
    n_x -- size of the input layer
    n_h -- size of the hidden layer
    n_y -- size of the output layer
    
    Returns:
    params -- python dictionary containing your parameters:
                    W1 -- weight matrix of shape (n_h, n_x)
                    b1 -- bias vector of shape (n_h, 1)
                    W2 -- weight matrix of shape (n_y, n_h)
                    b2 -- bias vector of shape (n_y, 1)
    """
    
    np.random.seed(2) # we set up a seed so that your output matches ours although the initialization is random.
    
    ### START CODE HERE ### (≈ 4 lines of code)
    W1 = np.random.randn(n_h, n_x) * 0.01
    b1 = np.zeros((n_h, 1))
    W2 = np.random.randn(n_y, n_h) * 0.01
    b2 = np.zeros((n_y, 1))
    ### END CODE HERE ###
    
    assert (W1.shape == (n_h, n_x))
    assert (b1.shape == (n_h, 1))
    assert (W2.shape == (n_y, n_h))
    assert (b2.shape == (n_y, 1))
    
    parameters = {"W1": W1,
                  "b1": b1,
                  "W2": W2,
                  "b2": b2}
    
    return parameters

 

4.3 The Loop

Gradient Descent를 반복적으로 수행하는데, FP / Cost 계산 / BP를 각각의 함수로 구현해보겠습니다.

 

- forward_propagation()

우선 FP 함수를 구현해봅시다. 위 NN 모델에서 봤듯이 우리는 hidden layer에서는 activation 함수로 tanh를 사용하고, output layer에서 sigmoid 함수를 activation function으로 사용합니다. 이것을 주의하면서 구현합니다.

여기서 parameters는 각 층의 파라미터들을 dictionary 타입으로 저장하고 있습니다.

# GRADED FUNCTION: forward_propagation

def forward_propagation(X, parameters):
    """
    Argument:
    X -- input data of size (n_x, m)
    parameters -- python dictionary containing your parameters (output of initialization function)
    
    Returns:
    A2 -- The sigmoid output of the second activation
    cache -- a dictionary containing "Z1", "A1", "Z2" and "A2"
    """
    # Retrieve each parameter from the dictionary "parameters"
    ### START CODE HERE ### (≈ 4 lines of code)
    W1 = parameters['W1']
    b1 = parameters['b1']
    W2 = parameters['W2']
    b2 = parameters['b2']
    ### END CODE HERE ###
    
    # Implement Forward Propagation to calculate A2 (probabilities)
    ### START CODE HERE ### (≈ 4 lines of code)
    Z1 = np.dot(W1, X) + b1
    A1 = np.tanh(Z1)
    Z2 = np.dot(W2, A1) + b2
    A2 = sigmoid(Z2)
    ### END CODE HERE ###
    
    assert(A2.shape == (1, X.shape[1]))
    
    cache = {"Z1": Z1,
             "A1": A1,
             "Z2": Z2,
             "A2": A2}
    
    return A2, cache

반환되는 값은 예측값 \(\hat{y} = A^{[2]}\) 이며, BP에서 사용하기 위해서 Z1, Z2, A1, A2를 저장해놓습니다.

 

- 두번 째로 위에서 구한 A2로 compute_cost() 함수를 구현합니다.

Cost는 아래와 같이 구할 수 있습니다.

\[J = -\frac{1}{m}\sum_{i = 0}^{m}(y^{(i)}log(a^{[2](i)}) + (1 - y^{(i)})log(1 - a^{[2](i)}))\]

 

numpy 내장함수를 사용해서 \(-\sum_{i = 0}^{m}y^{(i)}log(a^{[2](i)})\)를 다음과 같이 구할 수 있습니다.

logprobs = np.multiply(np.log(A2),Y)
cost = - np.sum(logprobs) 

첫번째 줄은 summation 내부 행렬의 요소곱 과정입니다. 그리고 두번 째줄에 행렬 요소의 합을 구합니다.

이렇듯, 우리는 모든 sample에 대해서 for문으로 구현할 필요없이 행렬연산으로 한 번에 원하는 값을 구할 수 있습니다.

 

# GRADED FUNCTION: compute_cost

def compute_cost(A2, Y, parameters):
    """
    Computes the cross-entropy cost given in equation (13)
    
    Arguments:
    A2 -- The sigmoid output of the second activation, of shape (1, number of examples)
    Y -- "true" labels vector of shape (1, number of examples)
    parameters -- python dictionary containing your parameters W1, b1, W2 and b2
    [Note that the parameters argument is not used in this function, 
    but the auto-grader currently expects this parameter.
    Future version of this notebook will fix both the notebook 
    and the auto-grader so that `parameters` is not needed.
    For now, please include `parameters` in the function signature,
    and also when invoking this function.]
    
    Returns:
    cost -- cross-entropy cost given equation (13)
    
    """
    
    m = Y.shape[1] # number of example

    # Compute the cross-entropy cost
    ### START CODE HERE ### (≈ 2 lines of code)
    logprobs = np.multiply(np.log(A2), Y) + np.multiply(np.log(1 - A2), 1-Y)
    cost = -np.sum(logprobs) / m
    ### END CODE HERE ###
    
    cost = float(np.squeeze(cost))  # makes sure cost is the dimension we expect. 
                                    # E.g., turns [[17]] into 17 
    assert(isinstance(cost, float))
    
    return cost

이 함수는 위 식을 계산해서 cost값을 반환합니다.

 

- 다음으로 BP를 수행하는 함수를 구현합니다.
BP는 딥러닝에서 가장 어려운 부분입니다. 정리는 이전 강의 정리내용에 있으므로, 참고바랍니다.

2020/09/08 - [Coursera 강의/Deep Learning] - Shallow Neural Networks

 

요약하면 다음과 같이 구할 수 있습니다.

미분항을 계산할 때, activation의 도함수를 계산해야하는데, \({g^{[1]}}'(Z^{[1]})\)를 계산해야합니다. 이때, g함수는 tanh함수이며, \(a = g^{[1]}(z)\)라고 할 때, g의 도함수는 \({g^{[1]}}'(z) = 1 - a^2\)으로 구할 수 있습니다. 따라서 \({g^{[1]}}'(Z^{[1]})\)는 (1 - np.power(A1, 2))로 구할 수 있습니다.

 

# GRADED FUNCTION: backward_propagation

def backward_propagation(parameters, cache, X, Y):
    """
    Implement the backward propagation using the instructions above.
    
    Arguments:
    parameters -- python dictionary containing our parameters 
    cache -- a dictionary containing "Z1", "A1", "Z2" and "A2".
    X -- input data of shape (2, number of examples)
    Y -- "true" labels vector of shape (1, number of examples)
    
    Returns:
    grads -- python dictionary containing your gradients with respect to different parameters
    """
    m = X.shape[1]
    
    # First, retrieve W1 and W2 from the dictionary "parameters".
    ### START CODE HERE ### (≈ 2 lines of code)
    W1 = parameters['W1']
    W2 = parameters['W2']
    ### END CODE HERE ###
        
    # Retrieve also A1 and A2 from dictionary "cache".
    ### START CODE HERE ### (≈ 2 lines of code)
    A1 = cache['A1']
    A2 = cache['A2']
    ### END CODE HERE ###
    
    # Backward propagation: calculate dW1, db1, dW2, db2. 
    ### START CODE HERE ### (≈ 6 lines of code, corresponding to 6 equations on slide above)
    dZ2 = A2 - Y
    dW2 = (1/m)*np.dot(dZ2, A1.T)
    db2 = (1/m)*np.sum(dZ2, axis = 1, keepdims = True)
    dZ1 = np.multiply(np.dot(W2.T, dZ2), 1 - np.power(A1, 2))
    dW1 = (1/m)*np.dot(dZ1, X.T)
    db1 = (1/m)*np.sum(dZ1, axis = 1, keepdims = True)
    ### END CODE HERE ###
    
    grads = {"dW1": dW1,
             "db1": db1,
             "dW2": dW2,
             "db2": db2}
    
    return grads

 

BP를 진행하고 미분값을 구했으면, 파라미터를 업데이트합니다.

\(\theta = \theta - \alpha\frac{\partial J}{\partial \theta}\)로 업데이트하며, 이때 \(\alpha\)는 learning rate 입니다.

만약 learning rate가 적절하다면, 알고리즘은 잘 동작하고 cost를 최소화하는 파라미터를 찾지만, learning rate가 너무 크면 제대로 동작하지 않을 것입니다.

# GRADED FUNCTION: update_parameters

def update_parameters(parameters, grads, learning_rate = 1.2):
    """
    Updates parameters using the gradient descent update rule given above
    
    Arguments:
    parameters -- python dictionary containing your parameters 
    grads -- python dictionary containing your gradients 
    
    Returns:
    parameters -- python dictionary containing your updated parameters 
    """
    # Retrieve each parameter from the dictionary "parameters"
    ### START CODE HERE ### (≈ 4 lines of code)
    W1 = parameters['W1']
    b1 = parameters['b1']
    W2 = parameters['W2']
    b2 = parameters['b2']
    ### END CODE HERE ###
    
    # Retrieve each gradient from the dictionary "grads"
    ### START CODE HERE ### (≈ 4 lines of code)
    dW1 = grads['dW1']
    db1 = grads['db1']
    dW2 = grads['dW2']
    db2 = grads['db2']
    ## END CODE HERE ###
    
    # Update rule for each parameter
    ### START CODE HERE ### (≈ 4 lines of code)
    W1 = W1 - learning_rate * dW1
    b1 = b1 - learning_rate * db1
    W2 = W2 - learning_rate * dW2
    b2 = b2 - learning_rate * db2
    ### END CODE HERE ###
    
    parameters = {"W1": W1,
                  "b1": b1,
                  "W2": W2,
                  "b2": b2}
    
    return parameters

 

4.4 Integrate 4.1, 4.2, 4.3 in nn_model()

앞서 구했던 함수들을 하나로 합치는 작업을 해봅시다. 

파라미터를 초기화하고, 매 iteration마다 forwad_propagation, compute_cost, backward_propagation을 실행하고, 구한 Gradient값으로 파라미터를 업데이트해주는 함수입니다.

 

# GRADED FUNCTION: nn_model

def nn_model(X, Y, n_h, num_iterations = 10000, print_cost=False):
    """
    Arguments:
    X -- dataset of shape (2, number of examples)
    Y -- labels of shape (1, number of examples)
    n_h -- size of the hidden layer
    num_iterations -- Number of iterations in gradient descent loop
    print_cost -- if True, print the cost every 1000 iterations
    
    Returns:
    parameters -- parameters learnt by the model. They can then be used to predict.
    """
    
    np.random.seed(3)
    n_x = layer_sizes(X, Y)[0]
    n_y = layer_sizes(X, Y)[2]
    
    # Initialize parameters
    ### START CODE HERE ### (≈ 1 line of code)
    parameters = initialize_parameters(n_x, n_h, n_y)
    ### END CODE HERE ###
    
    # Loop (gradient descent)

    for i in range(0, num_iterations):
         
        ### START CODE HERE ### (≈ 4 lines of code)
        # Forward propagation. Inputs: "X, parameters". Outputs: "A2, cache".
        A2, cache = forward_propagation(X, parameters)
        
        # Cost function. Inputs: "A2, Y, parameters". Outputs: "cost".
        cost = compute_cost(A2, Y, parameters)
 
        # Backpropagation. Inputs: "parameters, cache, X, Y". Outputs: "grads".
        grads = backward_propagation(parameters, cache, X, Y)
 
        # Gradient descent parameter update. Inputs: "parameters, grads". Outputs: "parameters".
        parameters = update_parameters(parameters, grads)
        
        ### END CODE HERE ###
        
        # Print the cost every 1000 iterations
        if print_cost and i % 1000 == 0:
            print ("Cost after iteration %i: %f" %(i, cost))

    return parameters

 

4.5 Predictions

이제 학습된 파라미터로 값을 예측합니다. 이전 실습과 마찬가지로 0.5보다 크면 1, 0.5보다 작거나 같으면 0으로 분류합니다.

이때 0.5는 threshold(임계값) 입니다.

# GRADED FUNCTION: predict

def predict(parameters, X):
    """
    Using the learned parameters, predicts a class for each example in X
    
    Arguments:
    parameters -- python dictionary containing your parameters 
    X -- input data of size (n_x, m)
    
    Returns
    predictions -- vector of predictions of our model (red: 0 / blue: 1)
    """
    
    # Computes probabilities using forward propagation, and classifies to 0/1 using 0.5 as the threshold.
    ### START CODE HERE ### (≈ 2 lines of code)
    A2, cache = forward_propagation(X, parameters)
    predictions = A2 > 0.5
    ### END CODE HERE ###
    
    return predictions

 

이렇게 구현한 nn_model로 X, Y 데이터와 hidden layer의 size를 4로 설정해서 학습하게 되면 아래와 같은 결과를 얻을 수 있습니다.

# Build a model with a n_h-dimensional hidden layer
parameters = nn_model(X, Y, n_h = 4, num_iterations = 10000, print_cost=True)

# Plot the decision boundary
plot_decision_boundary(lambda x: predict(parameters, x.T), X, Y)
plt.title("Decision Boundary for hidden layer size " + str(4))

꽤 적절하게 Decision Boundary가 나누어진 것을 볼 수 있습니다.

정확도를 계산하면 90%가 나옵니다.

# Print accuracy
predictions = predict(parameters, X)
print ('Accuracy: %d' % float((np.dot(Y,predictions.T) + np.dot(1-Y,1-predictions.T))/float(Y.size)*100) + '%')

Accuracy: 90%

 

Logistic Regression과 비교해서 확실히 높은 정확도를 보이고 있고, 꽃의 잎사귀 패턴대로 학습이 된 것을 볼 수 있습니다. 이렇게 NN은 non-linear한 decision boundary를 학습할 수 있습니다.

 

4.6 Tuning hidden layer size

방금까지는 hidden layer의 크기를 4로 설정했는데, 다르게 설정하면 결과가 어떻게 나오는지 확인해보겠습니다.

# This may take about 2 minutes to run

plt.figure(figsize=(16, 32))
hidden_layer_sizes = [1, 2, 3, 4, 5, 20, 50]
for i, n_h in enumerate(hidden_layer_sizes):
    plt.subplot(5, 2, i+1)
    plt.title('Hidden Layer of size %d' % n_h)
    parameters = nn_model(X, Y, n_h, num_iterations = 5000)
    plot_decision_boundary(lambda x: predict(parameters, x.T), X, Y)
    predictions = predict(parameters, X)
    accuracy = float((np.dot(Y,predictions.T) + np.dot(1-Y,1-predictions.T))/float(Y.size)*100)
    print ("Accuracy for {} hidden units: {} %".format(n_h, accuracy))

1,2,3,4,5,20,50의 경우를 살펴봅니다.

더 큰 모델일 수록 training set에 더 잘 맞는 것을 볼 수 있습니다. 

최고의 성능을 가진 모델은 hidden layer의 크기가 5인 모델입니다. 실제로 눈에 띄는 overfitting없이 주변 값들이 잘 맞는 것 처럼 보입니다.

더 큰 모델이지만, 만약 regularization을 통해서 학습한다면, hidden layer의 크기가 50이더라도 overfitting없는 모델이 될 수 있습니다.

 

5. Performance on other datasets

다른 data에 대해서 nn_model을 적용해보았습니다.

# Datasets
noisy_circles, noisy_moons, blobs, gaussian_quantiles, no_structure = load_extra_datasets()

datasets = {"noisy_circles": noisy_circles,
            "noisy_moons": noisy_moons,
            "blobs": blobs,
            "gaussian_quantiles": gaussian_quantiles}

### START CODE HERE ### (choose your dataset)
dataset = "noisy_moons"
### END CODE HERE ###

X, Y = datasets[dataset]
X, Y = X.T, Y.reshape(1, Y.shape[0])

# make blobs binary
if dataset == "blobs":
    Y = Y%2

# Visualize the data
plt.scatter(X[0, :], X[1, :], c=Y, s=40, cmap=plt.cm.Spectral);

parameters = nn_model(X, Y, 5, num_iterations = 5000)

plot_decision_boundary(lambda x: predict(parameters, x.T), X, Y)
predictions = predict(parameters, X)
accuracy = float((np.dot(Y,predictions.T) + np.dot(1-Y,1-predictions.T))/float(Y.size)*100)
print ("Accuracy for {} hidden units: {} %".format(5, accuracy))

hidden layer의 크기를 5로 설정했고, 매우 정확하게 decision boundary를 설정했습니다.

댓글