The Basics of a Neural Network Through the Eyes of a (Student) Mathematician
In this post, I’ll explain the fundamentals of a neural network in a way that a high school maths student, even with almost no computer science background, would be able to understand it as: an optimization problem.
So what is a neural network?
A neural network is a multivariable function that returns a multivariable answer.
To understand how this works, let’s start with a simple explanation.
In order to create the neural network, we need to identify a problem to solve. For machine learning models, the starting point is recognizing handwritten digits from the MNIST dataset, all numbers between 0 and 9.
All images are 28x28 pixel drawings with black backgrounds with grayscale drawings of numbers on top, so to make an input for our function, we’ll turn this 28x28 matrix of numbers into a 1x784 vector (meaning 1 row matrix), stacking each of the 28 rows next to each other:
______________________[
| 28 columns
| [0,0,0,0,0,....0],
| [0,0,0,0,0,....0],
28 rows | ...
| [0,0,0,0,0,....0]
|_____________________]
||
\/
_____________________[
| 784 columns
1 row | 0,0,0,0,0,....0
|____________________]
In the eyes of the neural network, as long as the 28x28 matrix is mapped to this 1x784 matrix in the exact same way every time, these 2 matricies are just two forms of the same thing. One version is for the human to understand it as a grayscale image, and the other version is the format used by the neural network for calculations.
As for the calculations, how does the neural network arrive at its answer?
Neural Network Parts
In this part, I’ll explain the basic parts we put into a neural network for it to work.
Neural networks work by sequentially activating layers of artificial nuerons to varying degrees. A final answer is reached after the network passes results through the last layer. A particular neuron performs 3 steps:
-
Compute a weighted sum of the values of all neurons it recieves numbers from. A given neuron determines how to weight each of the neurons inputting into it to prioritize some neurons’ values over others.
-
Apply a bias to the weighted sum calculated which is just a positive or negative number added to the result. Biases allow neurons that output values that are really high to reduce their output value, or allow neurons that don’t output really high values to increase their base value.
-
Apply an activation function to the weighted sum + bias value. Activation functions tell how a given sum+bias combination can transmit a value to the next set of neurons. For example, the sigmoid function turns the real number line into values from -1 to 1, with more extreme values coming closer to -1 and 1. For this model, we’ll use the Relu activation function, which turns all negative sums into the number 0 but leaves positive numbers alone.
This is repeated for every layer of neurons one layer at a time until the final layer outputs its answer, which in this case gives a 1x10 matrix. The values of the 10 cells are tied to the number the model thinks is being shown.
Expected values
0 => [1,0,0,0,0,0,0,0,0,0]
1 => [0,1,0,0,0,0,0,0,0,0]
2 => [0,0,1,0,0,0,0,0,0,0]
...
9 => [0,0,0,0,0,0,0,0,0,1]
What we get when we start running a untrained version of the network:
model => [0.456, 0.675, 0.986, 0.67...]
This jumbled mess of values is the beginning of turning this computer science question into a math one.
But How Is This Optimization?
In calculus classes, students learn that optimization problems are problems where derivatives are used to minimize or maximize the value of a single number through derivatives. In order to turn our training session into an optimization problem, we need to turn 10 numbers into 1. Our answer: the error function.
This error function should be as close to 0 as possible (in our case, above 1), but it starts out very high, since the model is made of random values for its biases and weights. To know how to modify the neural network we can take the gradient of the function Error(expected_value, neuralnetwork(neuron_values, input_matrix)) with respect to the values of the neurons in the current iteration of the neurons’ values. The gradient will return a matrix that gives the values needed to adjust the current neurons, but using these values directly will increase the error, not reduce it! Simply flip the signs to go in the direction that makes the error go down.
Gameplan to Make Our Neural Network
Before I show you how to make a neural network in python, let’s go over the key steps to this process (when understood, you can translate these steps to other coding languages):
- Get your dataset. For this project, I’ll be using the MNIST dataset as shown above, but this will work for any dataset that you can translate into arrays that your neural network can read. You’ll also need a way to translate the answers in the dataset (given as integers) into arrays to use in the error function. I showed a method two sections ago.
- Make your starting neural network. You’ll not only need to know the size of your input matrix and output matrix, but also decide how many layers and neurons per layer that you want.
- Write the function that takes in your input matrix and neuron values to make the output matrix.
- Write the function that trains your neural network. This has three parts: get the gradient matricies for a set number of randomly chosen inputs, take the average of those matricies to get one that approximates the best possible shift, and then makes the following adjustment.
- Test the model! It is good practice to separate training data from testing data to see how the model does with something new.
Making the Neural Network in Python
Before we begin, remember this exercise is merely a exercise in seeing how neural networks learn, and there are FAR better models that use libraries like pytorch to train faster, but I feel that seeing the inner workings of a basic model makes it easier to understand much more complicated concepts in that same field.
To begin, import the following libraries:
- Getting the dataset:
This turns the csv into easy to use arrays that we can input into our model.
- Making the starting neural network:
All of this is the work that allows us to run a neural network, now we just need to train it:
nd.Gradient runs our neural network a couple thousand times (because of the 1000s of variables in the neural network) to find the gradient at a particular point, so it’s important that our runbrain function runs as fast as possible. If I were to reprogram this, I would use some linear algebra tricks to perform a single matrix multiplication and addition per layer and speed this up.
But this is it! Now you have made (or copied, it’s okay as long as you understand it) your own little neural network that tries to recognize digits. At the end of this post, I’ve made my own Github repository so you can see this neural network in action!