0
$\begingroup$

I am trying to implement a simple neural network from scratch to classify images from the MNIST dataset. However, I have noticed that the efficiency of the code decreases as I try to train the network on a larger number of examples. Specifically, the performance of the function train_dataset() is problematic. I have gone through the code and think that the issue lies with the variables sum_d_a and sum_d that are initialized and updated in the for-loop inside train_dataset(). Here is the relevant code snippet:

def train_dataset(train_images, train_labels, activations, w, lr):
    m = len(train_images)
    deltas = [] # has deltas array for each training sample (yeah deltas in deltas)

    # getting deltas for each training sample
    for i in range(m):
        deltas.append(train_example(train_images[i], activations, w, train_labels[i]))

    # gradient descent
    for l in [3, 2, 1]:
        sum_d_a = np.zeros((w[0][l-1].shape)) 
        sum_d = np.zeros(w[1][l-1].shape)

        ####################### FINDING SUMS #####################################
        for i in range(m):  # for each training example
            sum_d_a += np.matmul(deltas[i][l-1], activations[l-1].T)
            sum_d += deltas[i][l-1]

        w[0][l-1] += lr * sum_d_a / m
        w[1][l-1] += lr * sum_d / m

I am not sure if this is the only part that is causing the inefficiency, but I have profiled the code and found that this loop takes a long time to execute. Could someone help me understand what is happening here and how I can fix it?

The whole code just in case:

import idx2numpy
import numpy as np

# utils
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_prime(x):
    return sigmoid(x) * (1 - sigmoid(x))

# Get data
train_images = idx2numpy.convert_from_file("data/train-images.idx3-ubyte")
train_labels = idx2numpy.convert_from_file("data/train-labels.idx1-ubyte")
test_images = idx2numpy.convert_from_file("data/test-images.idx3-ubyte")
test_labels = idx2numpy.convert_from_file("data/test-labels.idx1-ubyte")

train_images = np.reshape(train_images, (train_images.shape[0], -1)) / 255
train_labels = np.reshape(train_labels, (train_labels.shape[0], -1))
test_images = np.reshape(test_images, (test_images.shape[0], -1)) / 255
test_labels = np.reshape(test_labels, (test_labels.shape[0], -1))

# initialises variables: "activations" and "w"
def init_vars():
    activations = [
        np.zeros((784,1)),# input layer
        np.zeros((16,1)), # hidden layer 1
        np.zeros((16,1)), # hidden layer 2
        np.zeros((10,1))  # output layer
    ]

    w_01 = np.random.uniform(low=-1, high=1, size=(16,784))
    w_12 = np.random.uniform(low=-1, high=1, size=(16,16))
    w_23 = np.random.uniform(low=-1, high=1, size=(10,16))

    b_1 = np.random.uniform(low=-1, high=1, size=(16,1))
    b_2 = np.random.uniform(low=-1, high=1, size=(16,1))
    b_3 = np.random.uniform(low=-1, high=1, size=(10,1))

    w = [
        [w_01, w_12, w_23], 
        [b_1, b_2, b_3]
    ]

    return activations, w

# train NN
def train_example(input_layer, activations, w, ideal_output):
    """
    For each training example, we must go through each of the following steps:
    1. Feedforward: 
        - find z and a for each layer (layer indices: 1, 2, 3)
    2. Output error:
        - delta for last layer (the output layer)
    3. Backpropogate the error:
        - find delta for each layer with layer indices: 2, 1
    """
    
    # activations array has 3 elements, each is a layer's activation values
    activations[0] = input_layer  
    activations[0] = activations[0].reshape((784,1))

    # initialise z
    z = [
        np.zeros((16,1)),
        np.zeros((16,1)),
        np.zeros((10,1)),
    ]

    # FEEDFORWARD 
    for l in [0, 1, 2]: # l here is (l_index - 1) since thats how things are stored in other arrays
        a_l = activations[l]
        w_l = w[0][l]
        b_l = w[1][l]

        z[l] = np.matmul(w_l, a_l) + b_l

        activations[l+1] = sigmoid(z[l])  
        # here we actually did find lth layer's z, a 

    # OUTPUT ERROR
    del_C_wrt_a = ideal_output.reshape((10,1)) - activations[3]  # from defn

    # initialise deltas
    deltas = [
        np.zeros((16, 1)),
        np.zeros((16, 1)),
        np.zeros((10, 1))
    ]

    deltas[2] = del_C_wrt_a * sigmoid_prime(z[2]) # last layer

    # BACKPROPOGATION
    for l in [1, 0]:  # here l is (l_index - 1)
        w_next_l = w[0][l+1]
        deltas[l] = np.matmul(w_next_l.T, deltas[l+1]) * sigmoid_prime(z[l])
    
    return deltas  # 3 rows for each layer

def train_dataset(train_images, train_labels, activations, w, lr):
    m = len(train_images)
    deltas = [] # has deltas array for each training sample (yeah deltas in deltas)

    # getting deltas for each training sample
    for i in range(m):
        deltas.append(train_example(train_images[i], activations, w, train_labels[i]))

    # gradient descent
    for l in [3, 2, 1]:
        sum_d_a = np.zeros((w[0][l-1].shape)) 
        sum_d = np.zeros(w[1][l-1].shape)

        ####################### FINDING SUMS #####################################
        for i in range(m):  # for each training example
            sum_d_a += np.matmul(deltas[i][l-1], activations[l-1].T)
            sum_d   += deltas[i][l-1]  # our lth layer is computer's (l-1)th index

        w[0][l-1] -= (lr / m) * sum_d_a 
        w[1][l-1] -= (lr / m) * sum_d

# test NN
def test_nn(test_inputs, test_labels, w, activations):
    count = 0
    total = len(test_inputs)

    for i in range(len(test_inputs)):
        activations[0] = test_inputs[i]
        activations[0] = activations[0].reshape((784,1))

        z = [
            np.zeros((16,1)),
            np.zeros((16,1)),
            np.zeros((10,1)),
        ]

        for l in [0, 1, 2]: 
            a_l = activations[l]
            w_l = w[0][l]
            b_l = w[1][l]
            
            z[l] = np.matmul(w_l, a_l) + b_l

            activations[l+1] = sigmoid(z[l])

        if np.argmax(activations[3]) == np.argmax(test_labels[i]):
            count += 1

    print(f"accuracy = {count*100 / total}% i.e. {count} out of {total}")

# final output
def one_hot_encode(labels):
    result = np.zeros((labels.shape[0], 10))
    for i, label in enumerate(labels):
        result[i, label] = 1
    return result

if __name__ == "__main__":
    activations, w = init_vars()

    train_labels = one_hot_encode(train_labels)
    test_labels = one_hot_encode(test_labels)

    for _ in range(5):
        # generate a permutation of indices
        perm = np.random.permutation(len(train_images))

        # use the permutation to shuffle both arrays
        train_images = train_images[perm]
        train_labels = train_labels[perm]
            
        # split the dataset into 10 parts
        num_parts = 10
        part_size = len(train_images) // num_parts
        parts = [train_images[i*part_size:(i+1)*part_size] for i in range(num_parts)]
        label_parts = [train_labels[i*part_size:(i+1)*part_size] for i in range(num_parts)]

        for i in range(num_parts):
            # use each part of the dataset in each iteration

            train_dataset(parts[i], label_parts[i], activations, w, lr=0.01)
            test_nn(test_images, test_labels, w, activations)

These are some of the outputs:

accuracy = 9.87% i.e. 987 out of 10000
accuracy = 9.88% i.e. 988 out of 10000
accuracy = 9.87% i.e. 987 out of 10000
accuracy = 9.87% i.e. 987 out of 10000
accuracy = 9.88% i.e. 988 out of 10000
accuracy = 9.87% i.e. 987 out of 10000
accuracy = 9.87% i.e. 987 out of 10000
accuracy = 9.85% i.e. 985 out of 10000
accuracy = 9.83% i.e. 983 out of 10000

Also, let me know if you need to see other parts of the code to help diagnose the issue. Thanks in advance!

$\endgroup$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.