We don’t know what the desired mapping from context embedding vectors to feature scores actually looks like, so we will need to define the main part of our parametric function in such a way that it is flexible enough to represent a vast range of different relationships for different parameter values and hope that, given the right parameter values, it can approximate the desired "awesome feature extraction" function well enough.
For our toy model with 1D input and output, we said an intuitive way to achieve this would be to define a function made up of a bunch of continuously connected linear segments, whose boundaries, slopes and offsets are parameters we seek to optimize. Unfortunately, generalizing this to multidimensional inputs seems very non-trivial — even for a 2D input, it already isn’t clear how exactly we should define the boundaries of our linear regions, which would now be curves instead of points, let alone how to define our parameterized function in such a way that it is always continuous along all these boundary curves.
So while we could take on these challenges and proceed down a more mathematical route, let’s see if we can make our life easier and get inspiration from somewhere else instead.
The human brain
Ignoring the LLMs we’re reinventing, the only system that we know can perform sophisticated next-token prediction is the human brain, which means whatever type of function the human brain is implementing must definitely be powerful enough to do well.
Of course, we are far from understanding the human brain in detail and trying to recreate exactly what it is doing would likely be hopeless anyway, but just as we used some key observations about human development in coming up with our imitate + supervise + reinforce plan, we might be able to identify some basic patterns in how the brain functions and leverage them to tackle our problem.
The brain is largely comprised of two types of cells — neurons, which transmit electric signals and are the computational cells we are interested in, and glial cells, which support and protect neurons and serve all sorts of crucial auxiliary functions. Let's take a closer look at how neurons work.
A single neuron
Neurons are very diverse and can differ from each other in a million ways both big and small, but a typical neuron functions somewhat like this:
-
Inputs: It receives input (voltage) signals via terminals called synapses. These voltage signals can be both excitatory (promoting output) or inhibitory (suppressing output) and either stem from other cells (usually other neurons) or sometimes directly from the outside world.
-
Signal aggregation: Each of these input signals locally modifies the voltage near the synapse in question. Then, a complicated mess of biophysical processes determines how, if at all, these voltage changes at the synapses impact the voltage in the cell body, the so-called soma.
-
Thresholding: If the voltage in the cell body (soma) rises above a certain threshold, a biophysical chain reaction is triggered that causes a voltage signal to be sent to other neurons.
-
Output signal: For most neurons, this output signal is a so-called action-potential signal, which is binary — it is either sent or not sent, and the strength of the signal is always the same. However, the rate of these action-potential firings can vary and effectively correspond to different output signal strengths, since output signals can be integrated over small time windows by downstream neuronal circuitry.
So far, so good. But how are all these neurons wired together?
A biological neural network
We are far from understanding the brain in detail and no full map of all the neurons and their connections in a human brain has ever been made†The current state of the art for full-connectome reconstruction seems to be about of mouse brain, which is the sort of thing that sounds rather unimpressive if you don't think about it much and completely mindblowing if you do.[note], but we aren't trying to recreate the brain here, we just want to derive some inspiration from it to construct a powerful type of function that can learn to do many things.
So let's focus on just the simplest possible structural observations and see how far that gets us:
-
Many connections per neuron: How many other cells any given neuron connects to varies widely across the brain, but it's often many, varying from just input and output connections to other cells (for the most common type of neuron) all the way to neurons receiving inputs from or sending outputs to other cells. The type of neurons which mostly comprise the neocortex (where high-level reasoning is performed) tend to connect to other cells on both the input and the output side.
-
Layered structures: A recurring theme across different parts of the brain is that distinct layers of neurons can be identified. For example, both the neocortex (high-level reasoning) and the cerebellar cortex (learning and prediction) have clearly discernible layers of neurons and light impinging on our retinas gets processed into the brain by clearly distinguishable layers of cells as well. Even though tons of feedback loops via so-called recurrent connections and endless other complexities exist, a predominant direction of information flow can usually be discerned across these layers, with the outputs from one layer forming the inputs of another and so on.
Good enough. Let's see if we can leverage these insights to build up a simple but powerful function we can use for language modeling.
Artificial neural networks
If we would like to define a function inspired by the structure of the brain, we will first have to define some sort of mini-function that can act as a simplified, Artificial Neuron and then figure out how to make a bigger, more powerful function out of many such mini-functions, essentially wiring many artificial neurons together to form — drum roll — a Neural Network.
An artificial neuron
As mentioned above, biological neurons effectively receive inputs in a weighted fashion, allowing for both inhibitory and excitatory effects as well as varying sensitivity depending on the biophysical details of the synapses in questions.
The absolute simplest way to model this given some inputs would be to feed our artificial neuron a weighted sum of these inputs, i.e. to weight by some input weight , by some input weight , and so on. If we think of the various inputs and weights as elements of vectors, this weighted sum can simply be thought of as the dot product of a weight vector and the input vector, just like the weighted sum of the context-feature scores in the previous chapter:
where the weights are real-numbered parameters of our little artificial neuron model.
We also said that biological neurons will usually output a signal only if those weighted inputs add up to exceed some threshold , so let’s say the same should be true for our artificial neuron, meaning its output, which is usually referred to in machine learning as the neuron’s Activation , should be zero whenever the weighted input sum fails to exceed the neuron’s activation threshold:
In this framing, the threshold is a parameter of the so-called Activation Function determining the neuron’s output activation for a given sum of inputs. People usually prefer to think of the activation function itself as parameter-free and absorb the threshold parameter in the weighted sum instead, yielding an adjusted input sum known as the Preactivation :
But since writing things as simple sums is nicer than having mixed signs, this is more commonly written as
Here, we use the letter just like we did in the previous chapter, since we can think of the real-numbered parameter as a Bias added to the input of the artificial neuron, in the sense that a neuron with larger (more positive) bias is more likely to be active and a neuron with lower (more negative) bias is less likely to be active.
In this framing, in which the threshold is absorved into the preactivation itself, enforcing the thresholding requirement simply translates to enforcing that the activation of the neuron should be whenever its preactivation is or less:
But what if ? In other words, what should the output look like if the artificial neuron is actually active?
For biological neurons, we said the strength of the output tends to reflect the strength of the overall input, and that said output strength might be encoded in the frequency of output rather than the voltage of the signal. But encoding stronger signals in more frequently produced output and then integrating over time again seems completely inappropriate for our purposes — after all, we are not interested in simulating the brain’s dynamics, we merely want to define a simplified toy function roughly modeling the “net” computation implemented by the neuron.
So let’s avoid this frequency-encoding and simply define the output of our artificial neuron such that it is larger when the neuron's preactivation is larger. In other words, let's make sure our activation function is an increasing function. Of course, there are infinitely many such functions to choose from, but keeping with our philosophy of starting simple and making adjustments as needed, let’s just use what is arguably the simplest possible option — a linear function with slope one. Combining this with the thresholding requirement, we get
This can be written more compactly as simply choosing the larger value from and , and is known in machine learning as the ReLU function:
If we plot it, it looks like this:
The name derives from “Rectified Linear Unit”, with the unit in question being the artificial neuron and the word “rectified” indicating the thresholded behavior. Despite its simplicity, the function turns out to work remarkably well for machine learning.
Let's build an extremely simple Artificial Neuron by taking a weighted sum of inputs, adding a bias, and killing off the negative part of the result. The resulting mini-function is called a ReLU neuron, and its output is referred to as the neuron's Activation , whereas the weighted sum plus bias is called the Preactivation :
So far, so good! Having decided to define our artificial-neuron mini-function this way, let’s return to the brain to get some more guidance on how we might want to wire these artificial neurons together to hopefully get one big parameterized function that is sufficiently powerful to serve as the core of our language model.
An artificial neural network
Our main takeaway from our (incredibly superficial) look at the structure of the brain was that there was a persistent theme of densely connected layers of many neurons processing information.
Let's adopt this simple structure by constructing our overall function from many artificial neurons, each with its own input-weight and bias parameters, and by grouping those neurons in layers as well. In principle, these layers could vary in size and number, but to keep things as simple as possible, let's give each layer the same number of neurons for now and consider both that number and the number of layers hyperparameters we will have to choose values for when building an actual model.
A bunch of artificial neurons organized in layers.Naturally, the inputs of the first layer need to be the actual inputs. In our LM scenario, these will be the feature scores of our context embedding vector, but let's just consider them elements of a general real-number input vector for now, as we already did for the individual neuron above. To honor the general notion of "many connections per neuron", let's simply connect each input to all the neurons in the first layer. If we visualize this, it looks something like this:
The inputs are fed to the first layer of neurons via weighted connections (neuron biases not visualized).Algebraically, the output activation of the -th neuron in the first layer will then be
But writing this for every single neuron individually is very unwieldy, so let's condense our notation. First, let's pack all the input weights in one big first-layer weight matrix , with the -th row representing the input weights of the -th neuron:
Secondly, let's pack all the biases of the first layer neurons into a single vector as well. We can then write the output of the entire first layer as a vector which we can compute from the input vector like this:
Sweet. Let's keep building our network.
The whole idea of a layered structure is that the outputs from one layer serve as inputs to the next layer. If we once again connect everything to everything, meaning every neuron in a given layer to every neuron in the subsequent layer†Since each connection in our model has a weight parameter associated with it that can equal in principle, this everything-to-everything connection scheme is not just a simple choice but actually the most general layer-to-layer connectivity pattern possible.[note], it looks something like this:
Each layer's outputs serve as inputs to the subsequent layer.Each layer here is simply implementing the same type of operation as the first layer, except the inputs are the activations from the previous layer instead of the original inputs . Algebraically, the activations of the -th layer neurons are then given by
In contrast to the calculations performed by the brain, the main function we are trying to define for our LM scenario has a clear output, namely the vector of context feature scores our model subsequently uses to calculate the individual next-token scores.
So as a final step, we should map the outputs of our last layer of neurons onto this feature vector. Arguably the simplest way to do this is to once again use our simple weighted-sum-plus-bias recipe, so let's do that:
The outputs of the final layer are mapped to the outputs of the network via a final weighted-sum-plus-bias operation. No ReLU.In matrix lingo, this corresponds to performing the same type of operation we performed in each layer, except that we are not applying the activation functino here since we would like to be able to have negative outputs:
Et voilà! Using this simple layer-wise wiring, we have built a type of parameterized function that takes in a vector of real-numbered inputs (such as our context embedding vector) and produces a vector of real-numbered outputs (such as our context feature scores), with the parameters being all the individual input weights and biases of all the neurons and final outputs.
Functions like this, which are composed of many small units that can be loosely thought of as artificial neurons, are called Neural Networks, and the specific type we just invented is referred to as a Fully Connected Feedforward Neural Network, with the term “fully connected” indicating that each neuron is connected to all neurons in the previous layer, and the term “feedforward” indicating that no recurrent connections are present, meaning information flows through the network only in the forward direction, from input to output.
We can organize our artificial neurons in layers and use them to map a vector of inputs to a vector of outputs , with the output activations of each layer serving as inputs to the subsequent layer. Using neurons and matrix notation:
The layers of artificial neurons are usually referred to as the Hidden Layers of the network, since they can be thought of as being "hidden" between the inputs and outputs interfacing with the external world. Even though they do not contain any activation functions mimicking neurons, the inputs and outputs are also commonly framed as forming layers, which are simply referred to as the Input Layer and the Output Layer.
Neural networks are instrumental to modern machine learning and form a key part of most state-of-the-art AI models, including large language models like ChatGPT, but so far, our hope that they might be able to approximate a broad range of powerful functions is resting solely on the fact that we drew some loose inspiration from the brain in inventing them. So let's take a closer look at what all this corresponds to mathematically and see if this hope feels warranted.
Understanding networks
If we were to use a neural network as the core part of our model, it would ultimately have to take in a large context embedding vector as its input and, given the complexity of the task, would likely have to process that input through a number of intermediate layers with many neurons. But thinking about high-dimensional input spaces is tricky and visualizing them is virtually impossible, plus considering a bunch of layers at once is very overwhelming.
So let's start small.
1 input, 1 layer, 1 output
For now, let's consider only the simplest network we could think of — a network with just 1 input, 1 output, and 1 hidden layer of neurons in between.
Cute.The preactivation of the -th hidden-layer neuron in such a network is simply
where, as before, is the input weight for that neuron and is its bias. As a result, the activation of the neuron equals
As a function of the input , this corresponds to a linear function with slope and y-axis intercept whose negative part has been zeroed out. Let's play around with these parameter values a bit to get a feel for this:
Since all the weights and biases of the individual hidden-layer neurons are independent parameters, each neuron can represent an arbitrary function of this type, and the network as a whole can represent any function that can be written as a linear combination of these outputs (plus an optional bias ):
So let’s figure out what such linear combinations look like.
Picturing the output
A useful way of thinking about the single-neuron activation graph above is that each individual neuron output is made up of two pieces — the zero part, and the linear part — with each neuron having its own transition point , where the neuron's preactivation equals exactly zero and the neuron transitions from being inactive to being active or vice versa:
Each neuron has a specific threshold in input space where it becomes active.If we consider all the hidden-layer neurons simultaneously, these individual transition points partition the input space into a bunch of Regions, with each region corresponding to a specific and unique subset of neurons being active:
The individual neuron thresholds partition input space into regions where different subsets of neurons are active.Inactive neurons have zero output and might as well not exist, so for any one such region, the overall output of the network will simply be the weighted sum of the outputs of the active neurons. But each of those active neurons simply represents a linear mapping of the input, and adding a bunch of linear mappings together just gives another linear mapping!
This last fact is intuitive graphically since adding a bunch of lines of differing slopes together simply results in a new line whose slope is the sum of the individual ones, but it's also easy to show algebraically:
What this means is that the graph of the overall function that the network represents takes the form of a straight line for each region. But since the slope of that line depends on the weights tied to the active neurons, and since a different subset of neurons is active in each region, the slope of this line can vary from region to region!
The activation thresholds of the individual neurons effectively partition the input space into Regions. Within each region, a specific subset of neurons is active and the output of the network is represented by a straight line. If a region boundary is crossed, at least one neuron becomes active or inactive, and the slope of that line can change.
What’s more, since crossing the boundary between two regions corresponds to an individual neuron becoming active or inactive, and since any neuron’s output is continuous at that transition, the output of the network is guaranteed to be continuous at the boundaries of the regions (and is, of course, continuous within the regions already, as it corresponds to the output of a simple linear function in each region).
This is quite amazing. It means that a single-hidden-layer network with one input and one output represents a continuous, piecewise-linear function whose parameter values determine the widths and locations of the various linear regions as well as the slopes and offsets of the corresponding line segments. In other words, it represents the exact type of function we said would be an intuitive choice for the purpose of function approximation when discussing our simplified toy model with a single input and output in the previous chapter!
The reason we considered such a function desirable is that we can easily envision approximating any continuous function on some finite interval by using a bunch of linear segments if we can freely choose the segments’ slopes, offsets and boundaries.
This brings up the question whether our single-layer network allows for this freedom — is it merely the case that any such network represents some piecewise-linear, continuous function, or is it true that any such function can be implemented using an appropriately constructed network, and that we can therefore approximate virtually any function (on some finite interval) using a single-layer network?
Let’s find out.
Universal approximation
We have already established that the boundaries of the linear regions are located at the individual-neuron activation thresholds where the preactivations equal zero and the neuron in question turns on or off. This means we can always create a new region simply by adding another neuron, and we can freely choose the location of the corresponding boundary in input space by choosing the neuron's input weight and bias accordingly.
But what about the slope of the output in that new linear region?
We already determined above that the algebraic expression for the slope of any given linear region is
This means that whenever a newly added neuron becomes active, the slope will change by , which we can easily make arbitrarily small or large, for example by keeping fixed and choosing whatever value we desire for .
This implies we can tune the slope of the network output in the new region to be whatever we want, and suggests a simple recipe for approximating any continuous 1D function (on some finite interval) using a single-layer network: Simply add a new neuron with input weight whenever you would like to change the slope of the network output, using to ensure the neuron becomes active at the given point in input space, and using to adjust the slope of the overall output to your liking.†Not convinced? Simply play around with the biases and output weights in the widget above and convince yourself you can adjust all the segments at will.[note]
For example, here is what this would look like if we chose to approximate one period of a sine wave with five neurons:
Using a single-layer network, we can approximate an arbitrary 1D function on some interval by adding+activating an additional neuron wherever we'd like to adjust the slope.In math speak, this means single-layer networks are Universal Approximators of 1D functions — at least in theory, we can approximate any continuous function on some finite interval arbitrarily well using a single-layer network.
We can always introduce a new linear region by adding another neuron, and we can choose the weights of that neuron to adjust the slope of the network output as we see fit. So in theory, a single-layer net can approximate any continuous 1D function on some finite interval arbitrarily well, given sufficiently many neurons.
Given that we started this neural-network journey looking for a powerful function architecture that would be flexible enough to model whatever unknown functional relationship underlies next-token prediction, this is a highly encouraging result.
That being said, it isn’t more than that either — the mere fact that there must exist some hypothetical network approximating a function well neither means that it can be done with a reasonable amount of neurons nor that we will actually be able to find the right parameter values in practice, when the function we wish to approximate is unknown and we can’t simply look at a graph and eye-ball our slopes and boundaries. Plus, this is all assuming we are dealing with only 1 input and 1 output so far, whereas the inputs and outputs of the network we hope to use as the core part of our language model will have to be be many-dimensional.
So before we worry about anything else, let’s see if any of this promise even survives if we go beyond one-dimensional inputs, or if we have to make further adjustments to our function architecture.
inputs, 1 layer, 1 output
Let's say we have a context embedding dimension and our network must therefore deal with inputs. So for , for example, the network would look something like this:
A tiny two-input network.The preactivation of the -th hidden-layer neuron is then given by
where, as before, is the vector of inputs, is the vector of input weights for that neuron and is the neuron's bias. As a result, the activation of the neuron equals
Picturing the output
As before, the neuron will become active/inactive whenever the preactivation is exactly zero, meaning in this case whenever
With a single input , this condition simply yielded the individual boundary points . Let's see what it corresponds to in input space for the more general vectorial case.
Recall that the dot product of two vectors always equals the product of their magnitudes times the cosine of the angle between them:
A useful way of thinking about this equation is that is simply the signed scalar component of the vector along the vector :
is just how much the vector points along .So another way of thinking of the dot product of two vectors is that it equals the magnitude of one of the vectors times the scalar projection of the second onto the first:
Using this intuition, our above dot-product condition simply translates to
In other words, the preactivation will equal exactly and the neuron will become active/inactive whenever the scalar component of the input vector along the vector of the neuron's input weights equals a specific value:
But this only constraints the input vector in the direction of the weight vector. If a given input yields , taking a step in any direction in input space that is orthogonal to is not going to move the needle on the preactivation. So generally, the set of points in input space yielding for a given neuron will not just consist of isolated points but be of dimension .
For example, for a two-dimensional input (), the boundary across which a given neuron becomes active/inactive will take the form of a straight line in input space. If the neuron's bias equals , this line will simply be the line through the origin that is perpendicular to the direction of , since that is the line for which .
Single-neuron activation boundary for inputs if the neuron's bias is zero.If the neuron's bias is not equal , the activation boundary will still be perpendicular to the direction of , but offset from the origin along (or opposite) that direction to ensure that the scalar component of along equals .
Single-neuron activation boundary for for inputs if the neuron's bias is positive.Of course, the weight vectors and bias parameters for the individual neurons can and will all be different, so the activation boundaries of all the individual neurons will be different as well. Collectively, they will partition the input space into a bunch of regions defined by straight lines. For , this looks something like this:
Partitioning of input space for inputs. In each region, a different subset of neurons is active, just like in the 1D scenario.For three-dimensional inputs, these region boundaries would correspond to planes instead of lines, and for higher dimensions they will correspond to hyperplanes, but nothing about the basic logic changes. It's all based on dot-product intuition that readily extends to higher dimensions.
As before, each individual region will correspond to a specific subset of all the neurons being active. To figure out what the network output looks like as a function of the input for such a region, we can follow the same logic as before, the only differences being that our input is now a vector and each neuron has multiple input weight vectors , instead of merely multiple scalar input weights :
But by definition, the dot product of the "slope vector" and the input is just
meaning the output of the network is simply a linear function of all the inputs (with different slopes for each input) plus some offset!
In other words, going from a single input to many just means the output for any given region goes from looking like a straight line to looking like a plane (or hyperplane, if we have more than two inputs). It's still straight and sloped along a certain direction, it just has more dimensions. As before, this slope can change from region to region.
As in 1D, the activation thresholds of the individual neurons partition the input space into Regions, with a specific subset of neurons being active and the output being linear for each region. But the activation thresholds are now -dimensional hyperplanes instead of simple points, and the output in any given region is an -dimensional hyperplane segment instead of a simple line segment.
For inputs and neurons, this looks something like this:
Universal approximation
So far, so good. But the thing we are really trying to figure out is whether the universal-approximation property we observed for a single input still holds now that we have many.
Based on the observation that we can slice up the input space into arbitrarily many regions by adding more and more neurons to the hidden layer and that each neuron will give us more parameters to control the output across these regions, we might be optimistic that it does.
But it's far from obvious whether this is indeed true, because the activation of each neuron can only vary along a single direction in input space, namely that of its input-weight vector . When we only had a single input, having full control along a single direction was enough, and we could simply splice our desired function together step-by-step by introducing new region boundaries via additional neurons and adjusting the slope of the output as we saw fit. With a multi-dimensional input, we can still always add another neuron and use it to adjust the slope of the output in the half of input space where the neuron is active, but it's not obvious how we can use all the resulting neuron outputs to locally sculpt an arbitrary function.
Fortunately, there is a default recipe in math for constructing continuous functions from components that vary only along a single dimension: the Fourier transform. We will not go down the rabbit hole of explaining the intuition underlying Fourier transforms here, but for our purposes, the key insight is that any continuous function can be approximated arbitrarily well on some finite interval by a sum of plane waves, i.e. of functions that vary sinusoidally along only a single direction.
Using our 1D approximation recipe, we can approximate plane waves of arbitrary wavelengths pointing along arbitrary directions even in higher dimensional space, simply by choosing our neuron parameters such that all the activation boundaries are parallel and stacked in the direction along which the plane wave exhibits variation. Here is what this would look like for a 2D input space, for example:
So given a sufficiently large number of neurons to construct all these individual plane waves, we can theoretically make our network output approximate any continuous function arbitrarily well on some finite interval.[Don't like it?]
But by definition, the dot product of the "slope vector" and the input is just
meaning the output of the network is simply a linear function of all the inputs (with different slopes for each input) plus some offset!
In other words, single-layer nets are universal approximators even for many inputs.
In theory, we can approximate any plane wave arbitrarily well using our 1D approximation recipe, and given enough plane waves, we can approximate any continuous function on some finite interval arbitrarily well. So, given enough neurons, single-layer nets are universal approximators even for higher-dimensional input spaces.
inputs, layers, 1 output
So in principle, if we could have arbitrarily many neurons and were able to find the perfect set of parameter values, a single-layer network would be enough to approximate pretty much any function arbitrarily well.
But of course, we cannot afford to have arbitrarily many parameters in practice. Seeing as we already observed that the brain is partially comprised of layered structures and already hypothesized our artificial network should likely contain multiple layers as well, let's see what happens if we do add additional layers, i.e. if .
As always, let's start simple and hope general trends will reveal themselves. For and , the network would look something like this:
A tiny two-layer network.The equations mapping the inputs of such a two-layer network to its output are given by
Let's see what this does.
Picturing the output
As far as the first-layer neurons are concerned, absolutely nothing about our previous logic has changed. Each first-layer neuron will switch from being active to inactive (or vice versa) when its preactivation equals zero. As explained above, this will be the case along -dimensional activation boundaries. For our simple 2D-input example, this leaves us with the same straight-line boundaries as before:
Activation boundaries of first-layer neurons for .But what about the second-layer neurons? Looking at our network structure, we see that the preactivation of a second-layer neuron is calculated the exact same way the output of our single-layer network was calculated, meaning we can do the same exact algebra as earlier:
Complicated as this might look, the intuition is simple: Only active neurons matter. Along each individual active path through the network ending up at our neuron of interest, a given input will get weighted by the product of weights along the path. If we sum over all these paths for each input and lump all the resulting effective weights for the individual inputs into a vector, we get the above . Similarly, the overall bias of our preactivation is just the sum of the bias of the given neuron plus a weighted sum of the biases of earlier neurons.
Even though the above algebra is for the two-layer example, this intuitive understanding readily extends to networks with more layers, simply because it's always true that only active neural pathways matter and that weights get multiplied along those paths.
So what does this mean for our activation boundaries and the resulting linear regions?
For a fixed activation pattern in the earlier layers, the preactivation of a given neuron will equal zero along an -dimensional hyperplane that is orthogonal to the weight vector . This is simply due to the same dot-product logic we discussed earlier when figuring out the activation boundaries for a single-layer network. So for a two-dimensional input, the activation boundary for a fixed activation pattern in the earlier layers will still just be a straight line.
But crucially, the effective weight vector depends on which neurons are active in the earlier layers. So whenever we cross an activation boundary of an earlier-layer neuron in input space, the effective weight vector determining the orientation of our later-layer activation boundary will change! So for a 2D input space, the activation boundary of later-layer neuron is not just a straight line, but a straight line that changes direction whenever it crosses an earlier neuron's activation boundary:
example: The activation boundary of a second-layer neuron gets kinked whenever it crosses an activation boundary of a first-layer neuron (meaning a first-layer neuron becomes (in)active).If we plot this for a full two-layer network with neurons per layer and layers, we get four such kinked activation boundaries:
More second-layer neurons means more such kinked boundaries.If we add more layers, we simply get more and more activation boundaries which can get redirected more and more frequently, namely wherever they cross any activation boundary belonging to an earlier-layer neuron. For example, if we simply add two more layers, the regions in input space might look something like this:
Later-layer activation boundaries can get kinked at any earlier layer boundary.In higher dimensions, the same logic applies, it's just that instead of kinked lines, the activation boundaries of later-layer neurons will be kinked -dimensional hyperplanes. We can't really visualize it but it's the same exact idea – each first-layer activation boundary cuts the input space in half along one direction, every later-layer activation boundary does so until it encounters an earlier-layer boundary at which point its direction might change. As a result, the input space is divvied up into lots of regions in a parameter-dependent fashion.
The activation boundaries of all the neurons still partition the input space into regions, with a specific subset of neurons being active and the output being linear within each region. While the activation boundaries of first-layer neurons are -dimensional hyperplanes, the activation boundaries of later-layer neurons get "kinked" at the earlier-neuron boundaries, allowing for intricate partitionings of input space overall.
But what does the output look like for each of these regions?
Crucially, each of these regions corresponds to a unique activation pattern, meaning to a specific subset of the network's neurons being active, just as it did for our single-layer network. But if we restrict ourselves to such a fixed subset of neuron's being active to begin with, the function doesn't do anything at all and each layer simply outputs a linear combination of the activations from the previous layer plus some offset. Even if we chain many such layers and do this over and over again, the end result will still be a linear combination of the inputs plus some bias, exactly as it was for the single-layer network.
In other words, for any one region, we can still always write
meaning the output for each region still takes the form of an -dimensional hyperplane. For a network with inputs, the output looks like this:
Universal approximation
So far, so good, but what does this imply for the network's ability to flexibly approximate arbitrary functions?
In some sense, it doesn't change much. Given that single-layer networks are universal approximators, we already know that multi-layer networks must be, too, because any multi-layer network could always be wired to simply pass on the outputs of its first-layer neurons and act as a single-layer network. In other words, for the same number of neurons per layer, adding more layers can only increase the representational capacity of a network, never decrease it.
Adding more layers can complicate things in practice, but it can never diminish the network's representational capacity. So given that single-layer nets are universal approximators, multi-layer nets are, too.
But this universal-approximation property is just a theoretical guarantee for the limit of arbitrarily many neurons, and comparing multi-layer networks to single-layer networks with the same number of neurons per layer is comparing apples to oranges. In practice, more parameters will mean more complexity and more compute, so we will only be able to afford a certain number of them. So the question is what, if anything, adding more layers gets us, if we have to cut down the number of neurons per layer in turn.
Shallow vs. deep
Let's try to get some intuition for this by thinking about it in more mathematical terms.
A "wide" single-layer network with many neurons can be thought of as representing a parameterized function with a relatively large number of parameters.
If we restructure such a network to instead have multiple "narrower" layers with fewer neurons, we are effectively chaining together many functions with fewer parameters.
So the question is what's more desirable: Having a single, more powerful parameterized function operating directly on input space, or being able to chain a number of less powerful parameterized functions together?
If we assume both have the same amount of parameters, the answer to the question can only lie in how these parameters are used, so let's think about that.
For a single-layer network, the hidden-layer output activations are calculated by forming weighted sums of the network inputs, shifting those weighted sums by the neuron biases, and then killing off all the negative parts via the activation:
Visually, the weighted sums of the inputs simply correspond to -dimensional hyperplanes, meaning the resulting activations (after the killed off negative parts) are a bunch of -dimensional half-planes in input space.
The output of the network is then simply a weighted sum of these half-plane "building blocks", plus some shift:
Since each of these half-plane building blocks is tied to one of the neurons, each parameter†The only exception being the output bias, which simply shifts the output up or down.[note] of the network affects a single half-plane contribution to the output, whether it's by affecting the activation boundary where the half-plane starts (in the case of the neuron bias parameters ), by affecting the half-plane's slope (in the case of the output weights ), or by affecting both (in the case of the input weights ).
This means that, by and large, the network's parameters are on equal footing, and each of them has a semi-global impact on the output in this specific half-plane sense. While we have established that we can, in theory, approximate arbitrary functions if we combine enough such half-planes, this does not seem like a particularly efficient use of our parameters, both because most things aren't conveniently represented by sums of half-planes and because the fact that each parameter can only impact the output in a very specific way makes the network very inflexible.
For a multi-layer network, the situation is exactly the same in the beginning. The activations of the first hidden layer can be visualized as half-planes in input space. We just have fewer such half-planes now, because our multi-layer network must have fewer neurons per layer.
After that, each subsequent layer does more of the same: build weighted sums of its inputs, apply shifts, kill of negative parts.
But crucially, the inputs to each subsequent layers are the outputs of the previous layer. Meaning while the input-space building blocks for the second-layer activations are half-planes, the building blocks for the third layer are something built from half-planes, the building blocks for the fourth layer are something built from that, and so on.
From a parameter-usage point, this seems very intriguing. While parameters affiliated with first-layer neurons still determine the slopes and boundaries of a bunch of half-planes which are then fed to the second layer, the parameters of neurons in later layers are directly tied to far more complex functional landscapes across input space, namely whatever scalar functions the output activations from the previous layer happen to represent.
As these functional landscapes themselves are essentially outputs of a smaller neural net (namely the network up to that layer), they should be able to take near-arbitrary shapes given enough neurons. As a result, multi-layer nets should, in principle, be able to employ their parameters in a vastly more flexible fashion and generally be able to approximate functions much more efficiently, meaning using fewer parameters, than a single-layer net.†Of course, the parameterization will only be more efficient if the scalar functions represented by earlier-layer activations are actually useful building blocks, meaning all of this hinges on our ability to have our model learn the appropriate parameter values. As promised, we will start tackling this issue in the next chapter.[note]
Intriguingly, this argument does not even rely on our choice of the activation function or other minutae of our network. It's simply an outcome of the fact that we are processing the input information layer-by-layer with each layer representing a powerful parameterized mapping. This automatically allows later layers to operate on more complex mappings of the inputs and rather generally suggests that Deep networks of many layers might make for flexible and efficient function approximators. Indeed, this lies at the heart of the success of Deep Learning, which essentially means "machine learning with deep, multi-layer networks".
In deep (many-layer) neural nets, the output activations of neurons in later layers can correspond to increasingly intricate functions of the input. This allows later-layer parameters processing these activations to have a highly tailored and non-trivial impact across input space and usually allows for more parameter-efficient function approximation compared to shallower nets.
The scalar functions across input space represented by the activations of individual neurons are sometimes referred to as (hidden or learned) Features, which is related to but different from our earlier notion of a feature on which our tokens are scored. Unfortunately, the term feature is used in a number of related but different ways in ML contexts and people are often very sloppy about what exactly they are referring to.[more]
To visualize the notion of the network building increasingly intricate features from one layer to the next, as well as the basic idea of approximating arbitrary functions, here is a step-by-step animation of a simple net approximating Half Dome (a famous monolith in Yosemite National Park):
inputs, layers, outputs
Any network playing the role of in our model architecture would necessarily have to produce a large number of context feature scores, not just a single number.
So as a final step of our deep dive, let's examine what, if anything, changes if our network has more than one output. We already visualized this when we first wired our neurons together to form a full network earlier, but as a reminder, here is what it looks like:
Multiple inputs, layers and outputs. We have come full circle.Computationally, the only difference to the single-output scenario is that the activations of the final hidden layer are now mapped to a vector :
Picturing the output
For the single-output scenario, we determined that the network output could be visualized as a bunch of spliced-together plane segments, with each segment covering a region in input space corresponding to a specific subset of neurons being active.
Since we can think of each of the outputs of our multi-output network as the output of a single-output network, the same is still true now. We just have multiple such outputs, meaning we have to think of each of them as one such surface across input space.
Because the outputs are all computed as weighted sums of the same building blocks, namely the activations of the neurons in the final hidden layer, these individual output surfaces are not independent, however, and must all share the same activation boundaries.
Same as before: The activation boundaries of all the neurons partition the input space into regions, with a specific subset of neurons being active and the output being linear within each region. While the activation boundaries of first-layer neurons are -dimensional hyperplanes, the activation boundaries of later-layer neurons get "kinked" at the earlier-neuron boundaries, allowing for intricate partitionings of input space overall.
Here is what this might look like for a small network with inputs, outputs and neurons per layer:
Universal approximation
The extension of our previous universal-approximation logic is also straightforward: We've already established that we can approximate any single output arbitrarily well over some interval. If we would like to additionally approximate some other , we could simply add the required neurons to all the layers and avoid any cross-talk with the existing subnetwork by choosing all the cross-connecting weights to be zero and essentially having independent subnetworks. In this case our outputs would still be built from the same overall set of final-layer activations, but all the individual outputs would simply rely on independent subsets of those activations.
In principle, we could just have independent subnetworks for every single output. So given that nets are universal approximators for single outputs, the same is true for multiple outputs.
That said, this is once again just a theoretical guarantee for the limit of arbitrarily many neurons, and having a bunch of independent subnetworks would be horribly inefficient in practice.
Recall that the very hope underlying our choice to define a single function extracting high-level features from the input contexts was that the same features might be relevant to many different next-token probabilities. Having this function shared amongst tokens will help with compression exactly to the degree that our learning problem does not require independent sub-networks for all the individual token scores.
Bringing it all together
This was a lot, so let's briefly summarize the key takeaways and then tie everything back to our quest of building a better language model.
A brief summary
Taking loose inspiration from the human brain, we can define artificial neural networks by defining an "artificial neuron" mini function and wiring layers of such mini functions together to map an input vector to an output vector .
An intuitive and straightforward way to define such an artificial neuron is to have it form a weighted sum of its various inputs and enforce simple thresholding behavior, with the neuron becoming active only if said weighted sum is large enough. This is exactly what the function does.
Wiring a bunch of such neurons together in a sequence of layers yields a feedforward network.
For a fixed subset of neurons being active, such a network simply represents a linear mapping and its ouput can be visualized as a hyperplane segment. But whenever an activation threshold is crossed and some neuron in the network becomes active or inactive, the orientation of that plane segment can change, and so the overall output can be thought of as a bunch of tiled-together segments.
A closer examination of these types of outputs reveals that such networks are universal approximators, meaning given enough neurons, they could in principle approximate any continuous function on some finite interval arbitrarily well. Taken on its own, this basic theoretical fact implies little regarding their practical usefulness, but it does give us reason to hope they might be useful.
Another reason we might have that hope is that deep networks allow for later-layer parameters to be tied to highly intricate and tailored functional features across input space, which should be conducive to parameter-efficient approximations.
A neural-network-based architecture
This is all very promising, so let's give neural nets a try. For now, let's define our architecture's main function to be a feedforward net:


But wait a minute! The various in the above architecture are just simple "remixes" (weighted sums plus biases) of the final neuron activations, which we are then remixing again to obtain the token scores. This double remixing is completely redundant, so we might as well drop the intermediate step and replace the two-step mapping with a single step.†This is the same thing we already discussed earlier when analyzing the output of a network: Simply chaining multiple layers of weighted sums and biases together still results in a simple linear mapping, which is why for a fixed neuronal activation pattern, the output still just looks like a hyperplane even with many layers.[note]
This will remove the explicit stage of "context feature scores" able to take on any real-numbered value from our architecture, but we can still think of the output activations of the final neuron layer as representing non-negative context feature scores, and because the two formulations are equivalent, we are not losing any functionality at all:


Conceptually, this pretty much completes our quest for a (rudimentary) function-approximation architecture, but we are not quite ready to fully cross the model-definition bullet point from our list of urgent issues yet — defining our model does not just mean specifying the types of operations it should perform; it means specifying the exact mathematical function it represents. For our simple network, that means we need to decide how many layers to use and how many neurons to place in each layer.
This finding of suitable hyperparameter values is a different problem from the separate challenge of figuring out how to find suitable parameter values. We hope to find the latter via some sort of fitting procedure, but the former change the very structure of the function we are aiming to fit.
In the absence of any clear priors on what values our hyperparameters should take, this feels like an intensely empirical question, meaning we will simply have to figure out what model size and shape are best by training different models and comparing their performance down the road, once we have figured out how to perform such model training in the first place.
This only intensifies our pre-existing conviction that everything hinges on our ability to figure out a suitable method of finding appropriate parameter values though, so let's focus on that challenge in the next chapter.
