# Neural network basics, for graphics people

> Source: <https://ben3d.ca/blog/neural-network-basics>
> Published: 2026-08-18 00:00:00+00:00

# Neural network basics, for graphics people

A short reference on the neural-network vocabulary used across the neural texture, neural material and neural appearance posts — MLP, weights and biases, ReLU, forward pass, loss, gradient descent, backpropagation, Adam, latent vectors, feature grids and decoder networks — explained once, concretely, so those posts can link here instead of re-deriving it.

[Ben Houston](/about) • • 11 min read

This is a reference, not a story. It defines the neural-network vocabulary —
MLPs, training, gradient descent, Adam, latent vectors, feature grids and
decoder networks — once, carefully, and in plain terms. Nothing here is
specific to graphics. If you already know what a multilayer perceptron, Adam
and a latent vector are, you don't need this page. (It also underpins the [neural-texture compression blog post](/blog/neural-texture-compression-in-threejs).)

## A model is a function with dials on it[#](#a-model-is-a-function-with-dials-on-it)

A **model**, in this context, is nothing more than a
[function](https://en.wikipedia.org/wiki/Function_(mathematics)) with
adjustable numbers built into it: give it an input, it produces an output,
and the exact output for a given input depends on the current values of those
numbers. The numbers are the
[ parameters](https://en.wikipedia.org/wiki/Parameter) (also called

**weights** when they multiply something, and

**biases** when they just add a constant).

**Training** is the process of adjusting the parameters so the function's output matches some reference you already have. Nothing here requires the function to be a

["neural network"](https://en.wikipedia.org/wiki/Neural_network_(machine_learning))specifically — the same words describe fitting a line, a

[polynomial](https://en.wikipedia.org/wiki/Polynomial), or a GGX lobe to measured data. A neural network is just one particular shape of function, chosen because it can represent things a line or a polynomial can't.

## The multilayer perceptron (MLP)[#](#the-multilayer-perceptron-mlp)

An ** MLP** is a stack
of layers, where each layer does the same two things to its input: a
weighted sum, then a fixed nonlinear clamp.

is a matrix of weights, is a vector of biases, is the input vector, and is a fixed, non-learned function applied to every element of the result. Stack a few of these and you have an MLP:

Three layers. `x`

is the **input**, `y`

is the **output**, `h1`

and `h2`

are
**hidden layers** — "hidden" meaning neither the input nor the output, just
intermediate values nobody outside the function ever looks at directly. The
size of a hidden layer (how many numbers `h1`

holds) is called its **width**,
chosen before training and fixed afterward, because it determines the shape of
`W0`

and `W1`

. Running this function once, input to output, is a **forward
pass**.

Width and depth (number of layers) are two of the network's
**hyperparameters**, covered more fully below.

That's the entire architecture. An MLP is not more mysterious than this: a short, fixed sequence of matrix-multiply-then-clamp steps.

### ReLU, and why the clamp matters[#](#relu-and-why-the-clamp-matters)

is the most common
[ activation function](https://en.wikipedia.org/wiki/Activation_function):
identity for positive inputs, zero for negative ones. It's called the

[, hence ReLU.](https://en.wikipedia.org/wiki/Rectifier_(neural_networks))

**rectified linear unit** The clamp isn't decoration. Delete it and , , which is — a single matrix, no matter how many layers you stack. Every "deep" network without a nonlinearity between its layers collapses algebraically into one linear transform, and a linear function can't represent anything with a bend or a peak in it. ReLU is the cheapest nonlinearity that avoids that collapse, which is why it's the default choice unless something specific rules it out.

Each ReLU unit behaves like a hinge: it contributes nothing until its input crosses zero, then grows linearly. A layer of many hinges is a piecewise-linear function with that many possible folds, and stacking layers composes those folds into shapes a single layer couldn't reach. More width, more folds, finer approximation — at the cost of more parameters to fit and more work per evaluation.

## Loss, gradient descent, and training as fitting[#](#loss-gradient-descent-and-training-as-fitting)

You've done this before, informally: fit a polynomial through some points,
project a function onto
[spherical harmonics](https://en.wikipedia.org/wiki/Spherical_harmonics), tune
a GGX roughness parameter until a render looks right. All of those are the
same activity — a reference you trust, an approximation with free parameters,
and an adjustment process that reduces the gap between them.

Concretely, for an MLP:

- Run a forward pass on some inputs you have reference outputs for.
- Compare the network's output to the reference. That comparison, reduced to
a single number, is the
— a plain measure of how wrong the current parameters are. Squared error is the simplest common choice: , where is the network's output and is the reference value.**loss** - Compute how the loss would change if each parameter moved a little, in
which direction. That's the
, , of the loss with respect to the parameters (the collected weights and biases).**gradient** - Move every parameter a small step opposite its gradient — downhill on the
loss: . The step
size is the
.**learning rate** - Repeat, on a new batch of inputs each time.

One repetition of that loop is a training **step** or **iteration**. A group
of inputs processed together in one step is a **batch**, and its size is the
**batch size**. Unlike fitting a polynomial, there's no closed-form solution
for an MLP's parameters — you can't just solve for them directly — so training
nudges iteratively instead. This iterative process is
[ gradient descent](https://en.wikipedia.org/wiki/Gradient_descent).

### Backpropagation, briefly[#](#backpropagation-briefly)

Step 3 above — computing the gradient — is done with the
[chain rule](https://en.wikipedia.org/wiki/Chain_rule), applied from the
output backward through the layers. That algorithm has a name,
[ backpropagation](https://en.wikipedia.org/wiki/Backpropagation), but the
mechanics are ordinary calculus: each layer
receives an error signal from the layer after it, uses that signal to compute
its own weights' gradients, and passes a modified error signal further back.
Two consequences of this matter later. First, a ReLU's derivative is either 1
or 0 (it's linear where it's active, flat where it's clamped), so gradient
either passes straight through a unit or stops there completely — a unit whose
output is always negative can go permanently silent. Second, computing the
backward pass requires the values the forward pass produced at every layer, so
running backpropagation costs memory proportional to how many inputs you're
processing at once, not just to how many parameters the network has.

### Adam[#](#adam)

Plain gradient descent takes the same size step for every parameter, which
works poorly when some parameters consistently have large gradients and others
have small ones.
[ Adam](https://en.wikipedia.org/wiki/Stochastic_gradient_descent#Adam) is
an optimizer that fixes this by keeping two
running averages per parameter: the mean of recent gradients (so it keeps
moving through flat regions instead of stalling) and the mean of recent

*squared*gradients (so it can shrink the step for parameters whose gradient has been consistently large, and grow it for ones that have been small):

Dividing the first by the square root of the second gives each parameter its own effective learning rate without hand-tuning one per parameter ( is just a small constant that keeps the division from blowing up when is near zero). The practical cost: Adam needs two extra buffers the same size as your parameter set, one for each running average — a real memory cost when the parameter set includes a multi-megabyte grid of trained values, not just an MLP's weights.

## Latent vectors, feature grids, and decoders[#](#latent-vectors-feature-grids-and-decoders)

The three graphics posts this page supports all use one more pattern beyond a plain MLP, worth naming precisely because "latent" gets used loosely elsewhere.

A [ latent vector](https://en.wikipedia.org/wiki/Latent_variable) (or

**latent code**) is a set of numbers that started as free parameters — usually small random values — and were given meaning purely by training, rather than by any predefined encoding a person wrote down. They aren't RGB, they aren't roughness, they aren't anything you could name before training started; whatever structure they end up representing is whatever the optimizer found useful for minimizing the loss. This is different from an ordinary texture channel, where a person decided in advance that this number means roughness.

A **feature grid** is a 2D (or 3D) grid of latent vectors — one small latent
vector stored at every grid cell, addressed by a coordinate the same way a
texture is addressed by UV. Query it at an arbitrary coordinate and you
bilinearly interpolate the latent vectors at the surrounding cells, exactly
like sampling a texture, except what you get back isn't a color — it's a
latent vector that some other function still has to interpret. A feature grid
is itself a set of trained parameters, not a fixed input; it's optimized
alongside everything else during training.

A [ decoder network](https://en.wikipedia.org/wiki/Autoencoder) is the MLP
that turns a latent vector (or several,
concatenated) into something with an actual meaning again — RGB, a normal, a
BRDF response, whatever the task calls for. The pattern across all three
posts is: coordinate in, feature grid lookup, MLP decode, meaningful output
out. The grid supplies the "what's stored at this location," the decoder
supplies the "how do these stored numbers become a real answer," and both are
trained together so neither one has to be designed by hand.

### Trained parameters vs. everything else[#](#trained-parameters-vs-everything-else)

Worth being precise about one distinction that recurs: a **trained parameter**
is a number training is allowed to change — every weight and bias in the
decoder, every latent value in the grid. A
[ hyperparameter](https://en.wikipedia.org/wiki/Hyperparameter_(machine_learning))
is a number a person chooses before training starts and training never
touches — layer
width, learning rate, batch size, grid resolution, number of training steps.
Getting this backward is a common source of confusion: changing a
hyperparameter (like grid resolution) changes how many trained parameters
exist, but the hyperparameter itself is never something gradient descent
adjusts.

### Precision: fp32 during training, fp16 often on export[#](#precision-fp32-during-training-fp16-often-on-export)

One practical detail shows up in all three posts and is worth defining once.
Training typically keeps every parameter in
[32-bit floating point](https://en.wikipedia.org/wiki/Single-precision_floating-point_format)
(fp32), because the small updates gradient descent applies at each step need
that much precision to accumulate correctly over thousands of iterations.
Once training is done, the trained values are often exported at
[half precision](https://en.wikipedia.org/wiki/Half-precision_floating-point_format)
(fp16) — enough precision to reconstruct the result well, at half the storage
and bandwidth cost, and (in a WebGPU/WebGL context specifically) because
16-bit float textures are guaranteed hardware-filterable while 32-bit float
textures generally aren't without an explicit device feature. That's a
statement about what these systems actually do, not a universal law — whether
fp16 is "enough" depends on how sensitive the specific output is to rounding,
and each post that makes this trade explains why it holds for its own case.

## A short glossary[#](#a-short-glossary)

| Term | Meaning |
|---|---|
| Model | A function with adjustable parameters |
| Parameter / weight / bias | A number training is allowed to change |
| Hyperparameter | A number chosen before training and left fixed |
| MLP | A short stack of weighted-sum-then-clamp layers |
| Hidden layer | A layer that's neither the input nor the output |
| Activation function | The fixed nonlinearity applied after each layer's weighted sum |
| ReLU | , the most common activation function |
| Forward pass | Running the network once, input to output |
| Loss | A single number measuring how wrong the current parameters are |
| Gradient | How the loss would change if each parameter moved slightly |
| Gradient descent | Repeatedly stepping parameters opposite their gradient |
| Learning rate | The size of each gradient-descent step |
| Batch / batch size | A group of inputs processed together in one training step |
| Backpropagation | The chain-rule algorithm that computes gradients layer by layer |
| Adam | An optimizer that gives every parameter its own adaptive step size |
| Latent vector / latent code | Free parameters whose meaning is defined entirely by training |
| Feature grid | A grid of latent vectors, addressed and interpolated like a texture |
| Decoder network | The MLP that turns a latent vector into a meaningful output |

Keep these in hand and the rest of the series should read as engineering, not mathematics you have to take on faith.
