Imagine yourself as a realtor trying to use math to make your job easier. A new community has been built, but only the square footage and price of each home has been released. Now, you are looking to use this information to predict the prices of other new houses purely based on the square footage. To do this, you need to devise a model, or a function which takes square footage as an input, and calculates the house cost as an output. But this can’t be any random function, it must also accurately predict the prices of the houses down the street.
For this task, you could choose from a wide variety of function “types” ranging from linear functions all the way to complex trigonometric functions. But for simplicity, you decide on using a linear model. Now the goal is to find the line that best fits your data points.
We first have to create a metric that quantifies how wrong or how far away our line is from the best possible line, known as an error or loss function. There are many different options for this, but a popular and easy choice is Mean Squared Error (MSE):
Where:
MSE works by going over each data point and subtracting the predicted value from the actual value and then squaring the result.
Now the goal is to minimize the by finding the optimal slope and intercept of the line. By minimizing the error function, we can find the line that best fits our data points.
In order to minimize the error function and find the optimal line, we need to write the error function in terms of the slope and intercept.
First we denote the slope as and the intercept as . The predicted value can then be written as:
Where is the square footage of the house.
Then we can substitute this expression for into the MSE formula:
Since linear regression is a relatively simple task (only involving one input and one output with a simple linear model), it is possible to directly solve for the optimal slope and intercept using calculus.
The idea is that since MSE is a quadratic function, its minima can be found by setting the derivative with respect to and to zero and solving the resulting equations.
This is guaranteed to give us the minimum point / vertex of the MSE function because a quadratic function is convex (shaped like a U) and therefore has only one minima and no saddle points.
If we take a look at a normal quadratic function:
We can see that it has a single minima at . At this point, the derivative of the function is zero, and this makes sense because the slope of the tangent line at this point is horizontal.
Let's say our house data points are as follows:
| Square Footage (x) | Price (y) |
|---|---|
| 1500 | 300000 |
| 2000 | 400000 |
| 2500 | 500000 |
| 3000 | 600000 |
Now we want to model the best line that fits these data points.
We know the predicted value can be written as:
Therefore, we can evaluate the MSE function for our data points:
To find the optimal values of and , we take the partial derivatives of with respect to and , set them to zero, and solve the resulting equations.
Applying this to our data points:
Now we can take the partial derivative with respect to :
And applying this to our data points:
Now we have a system of two equations with two unknowns ( and ) that we can solve simultaneously. Solving these equations gives us the optimal values for and :
Simplifying these equations, we get:
Solving this system of equations, we find:
Verifying the results, we can see that this line perfectly fits our data points:
In real world scenarios, there are often multiple features that can influence the output. For example, in addition to square footage, factors such as the number of bedrooms, location, and age of the house can also impact its price.
Now instead of a single value as our input, we have a vector of features and instead of a single slope we have a vector of weights .
The predicted value can now be written as:
Or in vector form:
Where:
So far we have only considered the case of a single output (house price). However, in many scenarios, there may be multiple outputs to predict. For example, in addition to predicting the price of a house, we may also want to predict its rental value, or we might want to return a vector of probabilities for different classes in a classification problem.
In such cases, the model can be extended to handle mutiple outputs by using a weight matrix instead of a weight vector . The y-intercept is then extended to be a bias vector .
The predicted value can now be written as:
Where:
Does this look familiar? This is exactly how a single layer of a neural network operates! Each neuron in the layer computes a weighted sum of the inputs, adds a bias, and produces an output. By stacking multiple layers of such neurons, we can create deep neural networks capable of learning complex patterns in data.
A neural network is essentially a series of layers, where each layer consists of multiple neurons. Each neuron takes in multiple inputs, applies weights to them, adds a bias, and then passes the result through an activation function to produce an output.
If we go back to the multiple features section, we can see that this is exactly the equation for a single neuron in a neural network layer.
A single neuron gets its value from the dot product of the weights and the inputs, plus a bias.


After repeating this for multiple neurons in a layer, we get a vector of outputs. In effect, each layer of a neural network is performing a linear transformation on the input data.
However, if we only stack layers of linear transformations, the entire network would still be equivalent to a single linear transformation. This is where activation functions come into play. Activation functions introduce non-linearity into the model, allowing it to learn complex patterns in the data.
Some common activation functions include:
For example, the sigmoid function maps any input value to a value between 0 and 1, which is useful for binary classification tasks:
Another cool thing about activation functions is that they can apply operations that depend on the entire input vector, not just element-wise operations. For example, the softmax function is often used in the output layer of classification networks to convert raw scores into probabilities.
After neurons apply their weights and biases, the activation function is applied to all elements of the output vector.
In a simple linear function, we can get the output by just plugging in the input values into the equation. However, in a neural network, we have multiple layers of neurons, each applying weights, biases, and activation functions. The process of passing the input data through all these layers to get the final output is known as the forward pass.
First, the input vector is passed to the first layer of neurons. Each neuron computes its output by taking the dot product of the input vector and its weight vector, adding the bias, and then applying the activation function.
Often the input vector is normalized or standardized before being fed into the network to ensure that all features are on a similar scale. This helps with the convergence of the training process.
This produces an output vector for the first layer, which then becomes the input vector for the next layer. This process is repeated for each layer in the network until we reach the final output layer.
In practice, the forward pass is implemented with matrix multiplications for efficiency. Multiplying the input matrix by the weight matrix of a layer yields the same result as computing the dot product for each neuron individually.
The output vector from the final layer is the prediction of the neural network and can be interpreted based on the specific task (e.g., regression, classification).
Just like with our linear regression example, the goal is to find the optimal parameters that minimize the error function. In the case of linear regression, we could directly solve for the optimal slope and intercept using calculus. However, neural networks are much more complex and have many more parameters (weights and biases) to optimize, so directly solving for the optimal parameters is basically impossible.
Instead, we use an iterative optimization algorithm called gradient descent. The idea is to start with random initial values for the weights and biases, compute the error using a loss function (like MSE or cross-entropy), and then adjust the parameters in the direction that reduces the error.
We define a learning rate () which determines how big of a step we take going "downhill" on the error surface. Specifically we compute the gradient of the loss function with respect to each parameter, and then change move the parameters in the opposite direction of the gradient.
A simple one input one output function has a simple one value derivative, which tells us how the output changes with respect to the input. However, for a function with multiple inputs, a single value couldn't describe how the output changes with respect to each input. Instead, we use a vector of partial derivatives, known as the gradient.
The gradient of a function is a vector that contains the partial derivatives of the function with respect to each input variable:
If we go back to the linear regression example, we can see that the MSE function has two parameters: the slope and the intercept . Therefore, the gradient of the MSE function is a vector with two components:
And we solved for the optimal line by setting the gradient to zero and solving the resulting equations.
In a neural network, however, we have many more parameters, and multiple outputs as well. Now a vector can't describe how every output changes with respect to every input. Instead, we use a matrix of partial derivatives, known as the Jacobian.
The Jacobian of a vector-valued function is an matrix that contains the partial derivatives of each output with respect to each input:
For the weight matrix of a neural network layer, the Jacobian is just the weight matrix itself. This makes sense because the matrix is doing a linear transformation of the input vector.
Since the loss function outputs a scalar value, it's derivative is described by a gradient vector. However for each layer in the network, includng activation functions, we need to compute the Jacobian matrix in order to understand how the output of that layer changes with respect to its inputs.
Think of the loss function as a surface that maps the parameters of a neural network to a single error value. Therefore, to optimize the parameters, we need to find the lowest point on this surface.
Then, think of the gradient of the loss function as a vector that is tangent to this surface at a given point, and points in the direction of the steepest ascent. To minimize the loss, we need to move in the opposite direction of the gradient.

Now that we have the gradient, how do we use it to update the parameters? In practice, we don't plug in the entire model parameters into the loss function to compute the gradient. In models with millions or billions of parameters, this would be computationally infeasible. Instead, our error function only takes in the output of the model as the input, and we use the chain rule to compute the gradient of the loss function with respect to each parameter in the model.
Why does this work? Doesn't the loss function have to map our parameters to an error?
Remember that doing a forward pass involves passing the input data through each layer, each applying a linear transformation followed by a non-linear activation function. Therefore, our neural network is just a composition of functions.
Let's take a look at a simplified case of a neural network with two layers:
Where:
Now let's say we have a loss function that takes in the predicted output and the true output and outputs a scalar error value.
The gradient of the loss function with respect to the outputs is then:
But is a function of , , , and . Therefore, we can use the chain rule to compute the gradient of the loss function with respect to each parameter.
For the second layer activation function ():
Remember that the activation function maps a vector to a vector, so its derivative is described by a Jacobian matrix. Since a matrix multiplied by a vector results in another vector, the result is still a vector, which in this case is the gradient of the loss function with respect to the input of the activation function.
Let the vector be the inputs to the activation function. Then the derivative of the output of the neural network with respect to the inputs of the activation function is . This is just equal to the Jacobian of the activation function evaluated at . If we also recall that then by the chain rule we have:
Conceptually, it can be thought of that terms cancel out. The differentials are not actually numbers that can be cancelled, but it can be thought of that the Jacobian of the activation function scales the gradient of the loss function so it is now with respect to the input of the activation function. As a result, we can write:
Working backwards, we can now compute the gradient of the loss function with respect to the weights and biases of the second layer.
Currently, we have the gradient of the loss with respect to the input of the second activation.
Our goal is to find the gradient with respect to the weights, and then the biases, for the second layer. We want:
Now if we want to use the chain rule, we need to calculate , because by the chain rule we have:
So how do we calculate ? First, let's write the equation for the layer. Recall the equation from the forward pass:
How does each element of the vector change with respect to each element of the matrix ?
The element of is computed as:
Where is the row of the weight matrix .
So, the gradient of with respect to the weights in the row of is just the input vector . Therefore, the jacobian of with respect to is just 0 in all rows except for the row, which is equal to .
The element of is computed as the dot product of the row of and the input vector . Therefore, if we change any element in the row of , it will affect the value of . However, changing any element in a different row of will not affect , because it is not involved in the computation of that element. Therefore, the derivative of with respect to elements in other rows is zero.
If we repeat this for each element of we get a rank-3 tensor representing the derivative of with respect to . However, only the row of the matrix/slice is non-zero, and is equal to the input vector .
Now what happens when we multiply this tensor by the gradient vector ?
Multiplying by a tensor can yield different results depending on how the multiplication is defined. In this case, we want to define the multiplication such that we get a matrix of the same shape as . Let 's say the shape of is , meaning it has rows and columns. Then the shape of is , and the shape of is .
This makes sense because the weight matrix transformed our input column vector of n neurons from the previous layer into an output column vector of m neurons for the next layer.
This means that the shape of is also , since it has the same shape as , because this describes how the loss changes with respect to each output neuron.
When multiplying the vector with the tensor , we take all rows in the same spot in the tensor and apply a special weighted sum with the gradient vector to get an output vector. Then we repeat this for each row in the tensor to get a matrix.
What does it mean to take all rows in the same spot in the tensor? This just means taking the row from each slice/matrix of the tensor. Now to apply our special weighted sum, we take the first element of the gradient vector and multiply it by the row we are on in the first slice, then take the second element of the gradient vector and multiply it by the same row in the second slice, and so on. Finally we sum all these resulting vectors together to get a single output vector.
Remember that each slice of the tensor is the Jacobian of a single element of with respect to . Then one row in one slice represents the gradient of a single element of with respect to one row of . Therefore, when we take the same row from each slice, we are getting the gradient of all elements of with respect to one row of . Then, by multiplying each of these rows by the corresponding element in the gradient vector , we are weighting the contribution of each element of to the overall gradient with respect to that row of . Finally, summing these weighted contributions together gives us the total gradient of the loss with respect to that row of .
Now we repeat this operation for all the remaining rows to capture how the loss changes with respect to each row of . The result is a matrix of the same shape as , which is exactly what we wanted.
Recall that:
The gradient of with respect to the weights in the row of is just the input vector . Therefore, the jacobian of with respect to is just 0 in all rows except for the row, which is equal to .
Visualizing this, the Jacobian of with respect to looks like:
Where the row is equal to the input vector .
This is just one slice of the tensor , and we have that the row of the slice is equal to , while all other rows are zero.
So, when we do our weighted sum multiplication over the rows of the tensor, we can see that the only non-zero contribution to the sum for the row comes from the slice, because all other slices have a zero row in that position.
Therefore, the result of the weighted sum for the row is just the element of the gradient vector multiplied by the input vector .
Visualizing the output matrix, we have:
Where is the element of the gradient vector .
If we were to expand each row out, we would get:
Does this look familiar? This is just the outer product of the gradient vector and the input vector !
Normally when we multiply a vector of shape (m, 1) with a vector of shape (n, 1), we get a single scalar value representing the dot product of the two. However, if we want to get a matrix of shape (m, n), we can use the outer product.
This involves multiplying each element of the first vector with each element of the second vector to get a matrix where the element at position (i, j) is equal to the product of the i-th element of the first vector and the j-th element of the second vector. To do this we can just transpose the second vector to have shape (1, n) and then do a normal matrix multiplication.
The goal is basically to have one row vector and one column vector, so when we multiply them together, we get a matrix.
Why can't we just transpose the first vector instead of the second? This is because the first vector has shape (m, 1), so transposing it would give it shape (1, m). Then when we multiply it with the second vector of shape (n, 1), the inner dimensions don't match (m != n) and we can't do the multiplication. However, if we transpose the second vector to have shape (1, n), then the inner dimensions match (1 == 1) and we can do the multiplication to get a matrix of shape (m, n).
Therefore, we can compute the gradient of the loss function with respect to the weights of the second layer as:
Where denotes the outer product. This can be rewritten using the transpose as:
Recall the equation for the second layer:
Now the goal is to compute the gradient of the loss function with respect to the bias vector .
We want:
And we have the gradient of the loss function with respect to the pre-activation output of the second layer:
Now, to get the gradient with respect to the bias, we need to compute , because by the chain rule we have:
Calculating is straightforward. If we treat as constants, then the derivative of with respect to is just 1 for each element, because each element of changes by the same amount as the corresponding element of .
Therefore, the Jacobian of with respect to is just the identity matrix:
Where is the identity matrix of shape (m, m).
Now when we multiply the gradient vector by the identity matrix, we just get the same vector back:
So the gradient of the loss function with respect to the bias vector is just equal to the gradient of the loss function with respect to the pre-activation output of the second layer:
Now that we have the gradients with respect to the weights and biases of the second layer, we can continue this process backwards through the network to compute the gradients for all layers. First, we need to compute the gradient of the loss function with respect to the input () of the second layer.
Formally, we need to find . Since we already have , by the chain rule we need to compute .
Again, we can use the equation for the second layer:
The Jacobian of with respect to is just the weight matrix , because each element of changes by the same amount as the corresponding element of scaled by the weights.
We know that the shape of is (m, n), and the shape of is (m, 1). Therefore, we can't multiply them directly because the inner dimensions don't match (m != 1). However, if we transpose to have shape (n, m), then the inner dimensions match (1 == 1) and we can do the multiplication to get a vector of shape (n, 1).
Then, the gradient of the loss function with respect to the input of the second layer is:
Then we can compute the gradient with respect to the weights and biases of the first layer using the same method as before.
Where is the input vector to the network.
Now that we have the gradients for all layers, we can update the weights and biases using gradient descent.
Both the weights and biases are updated by subtracting the learning rate multiplied by the respective gradients.
From what we have done so far, we can make some psuedocode for the backpropagation algorithm:
There are multiple strategies for updating the weights and biases using gradient descent + backprop. The approach we covered is known as stochastic gradient descent (SGD), where we update the parameters after computing the gradients for a single data point. Then we loop over all the data points in the dataset, updating the parameters after each one.
In practice, we often use mini-batches of data points instead of a single point to compute the gradients. This is known as mini-batch gradient descent, where we update the parameters using the average of the gradients for a small batch of data points. This is a good balance between the efficiency of using the entire dataset (batch gradient descent) and the noise reduction of using a single data point (SGD).
After going through all data points (or mini-batches), we have completed one epoch of training. We can then repeat this process for multiple epochs until the model converges to a good set of parameters and low enough error.
A major challenge in training neural networks is overfitting, where the model learns to memorize the training data instead of generalizing to new data. This can happen when the model is too complex relative to the amount of training data, or when the training process goes on for too long.
To combat overfitting, we can use various regularization techniques:
In this post, we started with a simple linear regression example and derived the optimal parameters using calculus. We then generalized this to multiple features and outputs, which led us to the concept of neural networks. We saw how each layer of a neural network performs a linear transformation followed by a non-linear activation function.