Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Neural Network Quickstart

TLDR:

  • All statistical models are made of three parts:

    • A model that estimates things.

    • A way to measure how “bad” their estimates are.

    • A method to update the model parameters to make better estimates (hopefully).

  • Modern statistical models are coded in software

    • Enables scaling and iteration speeds never possible before.

    • Enables techniques for “guessing” new parameters through tools like autograd.

  • Neural networks are a type of statistical model

    • At their core, they are a lot of linear regression added together.

    • They’re put together in a way that can flexibly model all sorts of data.

  • This notebook contains all the code and libraries used for production scale LLMs

    • We use Jax to show you what the bare operations look like

    • We use Flax to show you the API that AI developers typically use

Neural Network Understanding From Linear Regression

Neural network models are at the core of modern GenAI systems, including GenAI. If you zoomed into a single neuron in a neural network you’d see this.

The core of all AI models

Figure 1:The core of all AI models

If it looks like a linear regression that’s exactly what it is. Now, with a model that’s y=mx+b y = mx + b , or a single neuron, it by itself can’t predict much. But if you take hundreds of thousands of linear regressions and you structure them into this thing called a transformer, It’s mindboggling how this basic idea can be scaled and the end result is Artificial Intelligence.

There’s more than just the model

However to really understand what’s going on you’ll need to know how to

  • Define a model

  • Measure its predictive performance

  • Update the parameters in a training loop.

For our linear model this is what the steps look like.

How all AI models work

Figure 2:How all AI models work

If you’re comfortable with these concepts skip to training an LLM from scratch

But if you’re not we’re going to learn the basics by implementing a linear regression in Flax.

By starting a simple model though we can focus on the basics, before scaling up to LLM sized models.

From words to code.

We’ll now implement a basic Linear Regression in Flax and the associated training loop in code.

Starting with our Linear Regression model we do the following:

  1. Generate some toy data to work with

  2. Create a linear regression model in Flax (a production Neural Network library that’s been used to train massive chat models)

  3. Implement the parameter learning loop

  4. Train the model

We’ll then extend our Linear Regression into a small neural network to show we can flexibly fit non linear data with a more complex model.

Run this Notebook Yourself!

I encourage you to run this notebook to get hands-on practice and reinforce your learning. Use the colab button on the right to get going with one click. We’ll talk through fundamental concepts in words first. Then we’ll go through the same concepts end-to-end in code,

Simple Linear Regression

Defining a model

For our linear model.

  • mm and bb are the parameters we’re going to need to estimate.

  • xx and yy are what we observed and we’re going to use these to figure out what mm and bb should be.

y=mx+by = mx + b

In this example we’re going to expand this a bit and add one more coefficient. For those curious we do this for shape reasons.

y=m0∗x0+m1∗x1+by = m_0 * x_0 + m_1 * x_1 + b

These are the numbers that are the “ground truth” for our simulation. In this simple case we can verify that our learning process worked correctly by seeing if the model infers these parameters. Again In real life we never really know these parameters, but we can use parameter recovery like this to debug our models.

Generate some observed y data

We’re first going to generate some sample data to fit against. We do this because in real life we’ll never know the “true” answers, there might not even be one. By generating data ourselves we “know” the answers and we can verify that our model got it right.

So now with m0m_0, m1m_1 and bb set we can calculate what yy will be if we pick any value of x0x_0 and x1x_1. Let’s pick some easy values first

5.2
4.2

Let’s pick some other values now to see what we get. Note how the values of the coefficients don’t change, it’s just x.

-0.7199999999999998

Generate Random X_0, X_1 points

In real life we won’t always see numbers on a grid. To simulate this we generate x values at random places. We’ll use these x values to estimate our y values.

array([[-5.45327955, -3.66483321], [ 5.94730915, 3.52509342], [-2.17780899, -3.34372144], [ 1.96617507, -6.26531629], [ 3.45512088, 8.83605731], [-5.03508571, 8.97762304], [ 3.34474906, -8.08204129], [-1.16320668, 7.72959839], [ 3.94907 , -3.47054272], [ 4.67856327, -5.59730089]])

Side Track: Fancy matrix multiplication

So if we want to do this with our first pair of x values and coefficients we could do it manually like this. But this is both ugly, error prone, and inefficient.

-10.594757237912647

Matrix Multiplication

So here’s a practical tip. If you’re going to work with Neural Nets you’re going to check shapes of things quite often. Let’s do that here.

((1, 2), (200, 2))

With these two shapes we can do a matrix multiplication But if you remember from Linear Algebra class the dimensions have to match like this.

(1,2) @ (2, 200) = (1, 200)

Let’s go ahead and do that in code

array([[-10.59475724], [ 17.04473623], [ -6.31740492], [ -7.89437163], [ 25.45635331]])

Einsum is really nice

We can also do the calculation above using einsum. This is an underrated feature in most tensor libraries, so I want to take this moment to show it to you here.

array([[-10.59475724], [ 17.04473623], [ -6.31740492], [ -7.89437163], [ 25.45635331]])

Let’s add some noise to Y

In real life we’ll never get exact measurements. Think of measuring something with a ruler or scale, there’s always a little bit of noise or error. To replicate this we add some random noise to our Y values.

What we’ve done up to this point

So far we haven’t done any modeling yet. We’ve just generated some simulated data to work with.

  • We decided our data generated function is a two coefficient regression with a bias

    • y=m0∗x0+m1∗x1+b y = m_0 * x_0 + m_1 * x_1 + b

  • Picked some arbitrary parameters

    • m_0, m_1 = 1.1, 2.1

    • intercept = bias = 3.1

  • We picked some random x_0 and x_1 observations and calculated our y1_observations

    • In the real world we’d have observed this

    • weight = m1*height + m2+width

  • We generated some random x_0 and x_1 points

    • Used that to calculate Y

  • Added some random noise to Y

    • The real world is messy

Building a Model in Jax

Let’s now code a model for our data. Reminder the goal here is to figure out the coefficients m_0, m_1, bias using our observed x_0, x_1, and y data.

Let’s do that using Jax and gradient descent. Gradient descent is important so let’s explain it here.

What is Jax

Jax is a numerical library that enables

  • Very quick computation of mathematical operations

  • Automatic gradient calculation

For training Neural Networks both of these properties are highly desirable, as we’ve mentioned, we want to quickly make estimates from our model, then figure out how to update the parameters so our estimates are better. Jax enables us to do both.

Estimating our parameters using gradient descent

  1. Start with some parameters that are probably bad

  2. Make a prediction

  3. Measure how bad that prediction is.

  4. Figure out which way to adjust the parameters so the prediction is less bad

  5. Change the parameters in direction

  6. Go to Step 2 again

Repeat this until your ability to make your guesses less bad stops.

This code block below contains

  1. A model

  2. A loss function

  3. A gradient computation function

Model, Estimate, Loss Calculation and Training Loop

Let’s now define all this in Jax and train our model.

(200, 2)
Learned weights: [[1.0531881]
 [2.0895174]]
Learned bias: 0.5365327
<Figure size 640x480 with 1 Axes>

And that’s it. This curve shows how over a number of steps our computer is able to find parameters for our simple regression which produce better and better estimates. Once the curve flattens out this indicates there’s not much more to learn and we can stop the training loop.

Now let’s do this all again in Flax.

Building the same model in Flax

With simple things like linear regression writing code by hand is fine. But when writing larger neural networks doing this by hand becomes tedious and error prone.

Flax is a neural network library that has been used to train some of the biggest models in the world. Now using Flax for a simple linear regression is a bit of an overkill, like driving a semi truck to pick up a carton of milk at the grocery store. But it’ll do the job as you’ll see below.

An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.

Initialize Random Parameters

{'params': {'dense': {'kernel': Array([[ 0.23232014], [-1.5953745 ]], dtype=float32), 'bias': Array([0.], dtype=float32)}}}
Loading...



With a model defined we can pick values of x and see what we get for y.

Array([0.23232014], dtype=float32)

L2 Loss by Hand

We’re going to use L2 loss, also known as mean squared error. This is a common error metric, but definitely not the only one.

Array([[ 0.23232014], [-1.5953745 ]], dtype=float32)
Array([4.5798745], dtype=float32)
array([-10.59475724])
Array([115.13471], dtype=float32)

Use Optax instead

Common loss functions like L2 are already defined in libraries like optax, so rather than having to handcode them we can just import and run them.

Array([4.5798745], dtype=float32)
Array(115.13471, dtype=float32)

Training Loop

Array(115.13471, dtype=float32)
flax.training.train_state.TrainState
TrainState(step=0, apply_fn=LinearRegression(), params={'params': {'dense': {'kernel': Array([[ 0.23232014], [-1.5953745 ]], dtype=float32), 'bias': Array([0.], dtype=float32)}}}, tx=GradientTransformationExtraArgs(init=<function chain.<locals>.init_fn at 0x7ff70c54a340>, update=<function chain.<locals>.update_fn at 0x7ff70c54a200>), opt_state=(ScaleByAdamState(count=Array(0, dtype=int32), mu={'params': {'dense': {'bias': Array([0.], dtype=float32), 'kernel': Array([[0.], [0.]], dtype=float32)}}}, nu={'params': {'dense': {'bias': Array([0.], dtype=float32), 'kernel': Array([[0.], [0.]], dtype=float32)}}}), EmptyState()))

Training Loss

<Figure size 640x480 with 1 Axes>

Final Parameters

With all that we now have our final parameters. Reference these against the coefficients we set above.

{'params': {'dense': {'bias': Array([3.1378474], dtype=float32), 'kernel': Array([[1.1043512], [2.0921109]], dtype=float32)}}}

What if our data is non linear?

So in our previous case we knew the relationship between x and y was linear because we generated it. In reality though most relationships are quite non linear, for example the relation between the pixels of an image, and the image being a cat. Or the relation between words in a sentence.

Here we’ll once again simulate data but this time with a non linear relationship between x and y.

<Figure size 640x480 with 1 Axes>

Initialize Params for our model

(100, 1)

We need to reshape X

array([[-10. ], [ -9.7979798 ], [ -9.5959596 ], [ -9.39393939], [ -9.19191919]])
{'params': {'dense': {'kernel': Array([[-0.92962414]], dtype=float32), 'bias': Array([0.], dtype=float32)}}}
flax.training.train_state.TrainState
{'params': {'dense': {'bias': Array([9.870098], dtype=float32), 'kernel': Array([[-7.460363e-08]], dtype=float32)}}}
Array([[9.870099], [9.870099], [9.870099], [9.870099], [9.870099]], dtype=float32)

This is a terrible fit

If we plot our original data, and what our model is able to estimate, we see that it’s not close at all. Our model can only really fit a line, but it’s too simplistic to fit a parabola.

<Figure size 640x480 with 1 Axes>

Expanding our model

Let’s expand our neural network so it can more flexibly fit data. We do this by adding a second layer. We also increase the number of parameters using the features keyword. We also include a relu layer. This is known as an activation function in a Neural Network. It’s what lets individual neurons turn themselves “on” or “off”

{'params': {'hidden_layer_1': {'kernel': Array([[-1.3402965 , 0.60816777, -0.06039568, -0.73402834]], dtype=float32), 'bias': Array([0., 0., 0., 0.], dtype=float32)}, 'hidden_layer_2': {'kernel': Array([[ 0.4993827 , -0.15674856, 0.19190995, -0.7276566 ], [-0.16007023, 0.4871689 , -0.32242206, 0.20559888], [ 1.0811065 , -0.07486992, -0.2228159 , 0.18825608], [-1.068188 , -0.01827471, -0.34845108, -0.7157423 ]], dtype=float32), 'bias': Array([0., 0., 0., 0.], dtype=float32)}, 'dense_out': {'kernel': Array([[-0.4820224 ], [-0.11561313], [-1.0410513 ], [-0.37862116]], dtype=float32), 'bias': Array([0.], dtype=float32)}}}
<Figure size 640x480 with 1 Axes>

Calculate Predictions

Plot Predictions

So here’s our fit. It’s better but it’s not great.

<Figure size 640x480 with 1 Axes>

Non Linear (but more complex)

Let’s now fit another model but we’ll increase the complexity by adding many more features, 128 in this case. Notice how the model is now able to fit the parabola “better”. It’s not perfect but this is the idea. Get a really big model with a lot of parameters and it can theoretically find the structure in anything, even the words on the internet in every language, including code.

<Figure size 640x480 with 1 Axes>

That’s all there is to create Neural Nets and LLMs. With that you’re ready for the next notebook where we recreate a Shakespeare LLM from scratch using the same tools here.

References