What a weight matrix actually does to a vector space

In our earlier article on how neural networks learn, we focused on how training adjusts weights and biases: backpropagation computes gradients, and gradient descent uses them to update the parameters. Here, we will explore the forward pass: how a network uses its current parameters to transform inputs into outputs. This computation takes place during both training and inference.

Vectors hold the representations, and weight matrices transform them. We will follow these operations through a classifier small enough to calculate by hand, starting with dots a straight line can separate and then turning to four criss-crossed XOR points where no line works. Separating the XOR classes with a linear output layer requires a nonlinear transformation of their representation. A hidden layer with hand-chosen weights will let us inspect how that happens.

In larger trained networks, knowing how to calculate every output does not automatically reveal what the internal representations encode or how the network uses them. Vectors provide a general way to encode objects, their properties, and relationships between them. The same mathematical tools can represent apple measurements for ripeness classification, image pixels for object recognition, or concepts and relationships for language understanding and generation.

Representational analysis studies what information internal vectors encode and how it is organized. Mechanistic interpretability investigates how a network’s components use that information to produce its behavior. The approaches overlap, but finding information in a representation does not by itself show that the network uses it.

This article assumes some familiarity with the following linear algebra concepts. The focus is on showing how a neural network uses them, rather than teaching them from scratch. Our dot-classification example will make their application concrete:

  • Vectors and matrices: how a vector represents one point, and how a matrix holds a collection of points.
  • Dot products: how each neuron combines its inputs with its weights to produce a scalar weighted sum, before adding the bias and applying any activation.
  • Linear transformations: how a weight matrix produces new coordinates, and what its rows and columns tell us about the transformation.
  • Matrix multiplication and composition: how successive linear transformations combine, and why a nonlinear activation prevents us from generally collapsing the whole network into one matrix.

A one-neuron classifier

Classical machine learning offers a variety of classification methods, including logistic regression, decision trees, and support vector machines. Here, we will implement a classifier using a neural network. Neural networks underpin deep learning; our small example will demonstrate the same building blocks while keeping every calculation manageable.

Let’s start with a dataset a single line can separate. Imagine four apples of the same variety, named A, B, C, and D. Each point in the dataset below represents one apple, described by two continuous measurements: a color score (x1x_1, from greener to redder) and a softness score (x2x_2, from firmer to softer). The labels tell us whether each apple is unripe (0) or ripe (1).

AppleColor (x1x_1)Softness (x2x_2)Class
A1-10.5-0.5Unripe (0)
B0.5-0.5+0.5+0.5Unripe (0)
C+0.5+0.5+0.5+0.5Ripe (1)
D+1+100Ripe (1)

The values we use for color and softness are illustrative and have been centered and scaled, so zero represents a reference level for each feature: negative values mean greener or firmer, and positive values mean redder or softer, relative to those reference levels.

Our first network has two inputs and one output neuron, with no hidden layer.

Two inputs and one output neuronOne apple’s color and softness feed a single neuron through weights w1 and w2. It adds the bias and predicts class 1 for a positive score, class 0 otherwise. There is no hidden layer.Input vectorOutput neuronClassx₁x₂w₁w₂Σ + bs > 0?0 or 1

The neuron computes a score for each apple, then applies a threshold to predict its class. We can organize the neuron’s two weights into a one-row matrix WW, and the apple’s two inputs into a column vector x\mathbf{x}:

W=[w1w2],x=[x1x2]W=\begin{bmatrix}w_1&w_2\end{bmatrix}, \qquad \mathbf{x}=\begin{bmatrix}x_1\\x_2\end{bmatrix}

To compute the score for one apple, we multiply the weight matrix WW by its input vector x\mathbf{x} and add the bias bb. We write the result as s(x)s(\mathbf{x}), where ss stands for score and x\mathbf{x} contains the apple’s color and softness measurements:

s(x)=Wx+b=[w1w2]×[x1x2]+b=w1x1+w2x2+bs(\mathbf{x})=W\mathbf{x}+b =\begin{bmatrix}w_1&w_2\end{bmatrix}\times\begin{bmatrix}x_1\\x_2\end{bmatrix}+b =w_1x_1+w_2x_2+b

Here, WxW\mathbf{x} computes one dot product between two vectors: the neuron’s weight vector (w1,w2)(w_1,w_2), stored as the row of WW, and the apple’s input vector (x1,x2)(x_1,x_2). Multiply each weight by its corresponding input, then add the two products. Adding the bias gives the output score.

The multiplication WxW\mathbf{x} is already a linear transformation from R2\mathbb{R}^2 to R\mathbb{R}: from a plane to a line. A transformation can turn a vector into a scalar; it need not keep the same number of coordinates. We will return to the geometric interpretation through basis vectors when we build the hidden layer.

This model is a perceptron, a single-neuron linear classifier. Its step activation turns the score into a predicted class label, y^\hat y:

y^=step(s(x))={1if s(x)>0(ripe),0if s(x)0(unripe)\hat y=\operatorname{step}(s(\mathbf{x}))= \begin{cases} 1 & \text{if }s(\mathbf{x})>0 \quad \text{(ripe)},\\ 0 & \text{if }s(\mathbf{x})\le0 \quad \text{(unripe)} \end{cases}

We chose 1 to mean ripe and 0 to mean unripe when labeling the dataset. One output neuron is enough: a positive score produces label 1, while a zero or negative score produces label 0.

Let’s now visualize how our neuron classifies the four apples. In the default 2D view, each apple’s color and softness determine its position in the plane. The shaded background shows the predicted class for every possible input pair, with a straight line separating the unripe and ripe regions. Each marker’s color shows the apple’s known label; a pink ring marks a wrong prediction.

Adjusting the weights and bias changes the decision regions while the apples stay fixed. We chose their measurements to make the classes linearly separable, so suitable parameter values place all four apples in their correct regions.

Switch to Angled view (3D) to see the scores behind those predictions. The input plane holds the apples’ color and softness coordinates, while the blue plane shows the calculated score as a height above or below it. Where the two planes intersect, the score is zero: this is the same boundary line shown in 2D. For a closer look at how these views are constructed, see how to read the neural network graphs.

s(x) = 1.00x₁ + 1.00x₂ − 0.50

4 of 4 classified correctly

1.00
1.00
-0.50
x₁-11x₂-11Apple A: Color -1, Softness -0.5, Score -2.00, Prediction 0Apple B: Color -0.5, Softness 0.5, Score -0.50, Prediction 0Apple C: Color 0.5, Softness 0.5, Score 0.50, Prediction 1Apple D: Color 1, Softness 0, Score 0.50, Prediction 1ABCD
Unripe (0)Ripe (1)

The formula above the plot is the neuron’s score calculation with the current parameters substituted. The sliders control the weights w1w_1 and w2w_2 and the bias bb shown in the network diagram. These are the parameters training would learn.

In our earlier example of fitting a line, one neuron learned one weight and a bias. Here there are two input measurements, so there are two weights and a bias. The same gradient-descent idea applies: a differentiable loss on the scores and known labels supplies gradients for updating the parameters; the hard threshold reports class predictions.

Let’s now trace the calculation behind the line shown in the widget. At the widget’s initial settings, w1=1w_1=1, w2=1w_2=1, and b=0.5b=-0.5, so the score is s(x)=x1+x20.5s(\mathbf{x})=x_1+x_2-0.5. Its decision boundary contains the points whose score is zero:

x1+x20.5=0x_1+x_2-0.5=0

That is a straight line. For example, for point C at (0.5,0.5)(0.5,0.5), we substitute x1=0.5x_1=0.5 and x2=0.5x_2=0.5 into the score formula: s(xC)=0.5+0.50.5=0.5s(\mathbf{x}_C)=0.5+0.5-0.5=0.5. The score is positive, so the prediction is ripe (1). For point B at (0.5,0.5)(-0.5,0.5), we substitute x1=0.5x_1=-0.5 and x2=0.5x_2=0.5: s(xB)=0.5+0.50.5=0.5s(\mathbf{x}_B)=-0.5+0.5-0.5=-0.5. The score is negative, so the prediction is unripe (0). The line separates the whole plane into those two kinds of answers.

We can also compute the scores for all four apples in one matrix multiplication. This is how we process a batch: collect the input vectors into a matrix XX, with one apple per row and color and softness in the two columns:

X=[10.50.50.50.50.510]X=\begin{bmatrix} -1&-0.5\\ -0.5&0.5\\ 0.5&0.5\\ 1&0 \end{bmatrix}

The parameters are still the same two weights and one bias. If we start with w1=1w_1=1, w2=1w_2=1, and b=0.5b=-0.5, as in the widget, the weight matrix and its transpose are:

W=[11],W=[11]W=\begin{bmatrix}1&1\end{bmatrix}, \qquad W^\top=\begin{bmatrix}1\\1\end{bmatrix}

The bias b=0.5b=-0.5 is separate from WW and is added to each apple’s weighted sum.

For one apple written as a column vector, we used WxW\mathbf{x}. With apples stored as rows, we use XWXW^\top: the transpose turns the neuron’s weight row into a column, so each apple’s row takes a dot product with the same weights. Adding the same bias to every score gives:

S=XW+b1=[10.50.50.50.50.510]×[11]+[0.50.50.50.5]=[20.50.50.5]S=XW^\top+b\mathbf{1} =\begin{bmatrix} -1&-0.5\\ -0.5&0.5\\ 0.5&0.5\\ 1&0 \end{bmatrix} \times\begin{bmatrix}1\\1\end{bmatrix} +\begin{bmatrix}-0.5\\-0.5\\-0.5\\-0.5\end{bmatrix} =\begin{bmatrix}-2\\-0.5\\0.5\\0.5\end{bmatrix}

Here, 1\mathbf{1} is a column of four ones, so b1b\mathbf{1} repeats the bias for all four apples. The rows of SS contain their scores in A–D order. For apple A, the first row computes 1(1)+1(0.5)0.5=21(-1)+1(-0.5)-0.5=-2, exactly as when we process it alone. Applying the step function to each score gives predictions (0,0,1,1)(0,0,1,1), all correct. Batching does not change how the score is calculated for an individual apple. Our batch contains the entire four-example dataset; larger datasets are often split into smaller batches, as discussed in how batch size affects training.

Four apples one neuron can’t separate

Let’s continue with our four apples, still described by color (x1x_1) and softness (x2x_2). We change their input values (x1,x2)(x_1,x_2) by hand while keeping their labels: A and B remain unripe (0), and C and D remain ripe (1). The new coordinates put each class on a diagonal of a square:

AppleColor (x1x_1)Softness (x2x_2)Class
A0.5-0.50.5-0.5Unripe (0)
B+0.5+0.5+0.5+0.5Unripe (0)
C0.5-0.5+0.5+0.5Ripe (1)
D+0.5+0.50.5-0.5Ripe (1)

This is the familiar XOR pattern. We have shifted the usual binary coordinates, 0 and 1, to 0.5-0.5 and +0.5+0.5, placing them on opposite sides of zero on each axis. The four apple points form a square centered at the origin: apples with opposite signs for their two input values are labeled ripe (1), while those with matching signs are labeled unripe (0).

Each class occupies a different diagonal of that square. The diagonals cross, so no straight line can place all unripe apples on one side and all ripe apples on the other. This is a classic illustration of the perceptron limitations analyzed by Marvin Minsky and Seymour Papert in their 1969 book Perceptrons.

When we put the apples from the updated dataset through our original classifier, the same parameters w1=1w_1=1, w2=1w_2=1, and b=0.5b=-0.5 now classify only one of four correctly. Open the widget’s parameter controls and try changing w2w_2 to 1-1: this reaches three of four, with C still misclassified.

Adjust weights and bias
1.00
1.00
-0.50

s(x) = 1.00x₁ + 1.00x₂ − 0.50

x₁-11x₂-11Apple A: Color -0.5, Softness -0.5, Score -1.50, Prediction 0Apple C: Color -0.5, Softness 0.5, Score -0.50, Prediction 0Apple D: Color 0.5, Softness -0.5, Score -0.50, Prediction 0Apple B: Color 0.5, Softness 0.5, Score 0.50, Prediction 1ABCD

To solve XOR, we will keep the two inputs and one output neuron, and insert a hidden layer with two neurons that use the nonlinear ReLU activation. This layer transforms each apple’s measurements into new coordinates (h1,h2)(h_1,h_2), where the output neuron can separate the classes with a straight line:

Two inputs, two hidden neurons, and one output neuronBoth inputs connect to both hidden neurons through W. Each adds its bias and applies ReLU to produce h1 or h2. The output neuron combines them through V, adds c, and predicts class 1 for a positive score, class 0 otherwise.Input vectorHidden layerOutput neuronWVx₁x₂h₁Σ + b₁ReLUh₂Σ + b₂ReLUΣ + cs > 0?Class: 0 or 1

The illustration shows a multilayer perceptron (MLP): a network of fully connected layers, where each neuron receives all outputs from the previous layer. It is also a feed-forward network (FFN), meaning information flows from input to output without looping back. Other architectures use different kinds of layers. Convolutional neural networks (CNNs) use convolutional layers that apply shared filters to local regions, and can also include fully connected layers, for example to perform the final classification. Recurrent neural networks (RNNs) use recurrent layers that carry state between sequence steps.

We will now explore the linear algebra behind this network and build its forward pass in NumPy as we go. We will use individual apples to work through the calculations, then apply each operation to all four as a batch. The complete NumPy example brings those operations together.

Representing our apples as vectors and matrices

Each apple is represented as a vector of two numbers: its color score and its softness score. We use lowercase x\mathbf{x} to denote one apple’s input vector, containing its color score x1x_1 and softness score x2x_2. For apple C, we write this vector as a column:

xC=[0.50.5]R2\mathbf{x}_C = \begin{bmatrix} -0.5 \\ 0.5 \end{bmatrix} \in \mathbb{R}^2

R2\mathbb{R}^2 is the space of all pairs of real numbers. It is two-dimensional because two independent coordinate directions suffice to locate any point: move along the first axis, then along the second.

We can draw the vector as an arrow from the origin or mark its endpoint as a dot. Both pictures encode the same coordinates. Here the dot is useful because we are classifying examples.

Apple C as a vector in the input planeAn arrow runs from the origin to apple C at color −0.5 and softness 0.5. Dashed guides connect its endpoint to those values on the axes. The arrow and its endpoint represent the same pair of coordinates.Softness (x₂)Color (x₁)C(−0.5, 0.5)

Start the NumPy example with this representation of apple C:

import numpy as np

# One apple's representation: color and softness, in that order.
x = np.array([-0.5, 0.5])        # Shape (2,)

We write xC\mathbf{x}_C as a column in the mathematics. In NumPy, this one-dimensional array supplies the input vector when we multiply it by WW.

The representation space is the whole plane. Our dataset contains just four points in that plane. Having four examples does not make the space four-dimensional; each example still has two coordinates.

We can collect the four vectors into a representation matrix, with one example per row. Uppercase XX denotes the matrix holding all four apples, with one input vector per row.

X=[0.50.50.50.50.50.50.50.5]R4×2X = \begin{bmatrix} -0.5 & -0.5 \\ 0.5 & 0.5 \\ -0.5 & 0.5 \\ 0.5 & -0.5 \end{bmatrix} \in \mathbb{R}^{4 \times 2}

The rows count examples (our four apples); the columns count features (color and softness). The ripeness labels are stored separately.

All four apple vectors in the same input planeFour arrows run from the origin to A at (−0.5, −0.5), B at (0.5, 0.5), C at (−0.5, 0.5), and D at (0.5, −0.5). Teal circles mark unripe apples A and B; coral squares mark ripe apples C and D. All four vectors have two coordinates.Softness (x₂)Color (x₁)A(−0.5, −0.5)B(0.5, 0.5)C(−0.5, 0.5)D(0.5, −0.5)

These are the same four input positions shown in the XOR widget. The arrows make explicit that every point represents a vector from the origin.

A representation matrix can hold vectors from the original input data, as XX does here, or vectors produced by a previous layer. In both cases, each row represents one example; what changes between layers is the information encoded in its coordinates, and possibly the number of coordinates. We distinguish this role from that of a transformation matrix: XX holds the representations, while the weight matrix WW defines the linear transformation applied to them.

Our apples already come with two numerical measurements, so forming their input vectors is straightforward. For other kinds of data, we need to decide how to represent each example as a vector. A 28×2828 \times 28 grayscale image can become a vector of 784 pixel values in a fixed order, while a token can be represented by an embedding vector. In each case, the number of coordinates in the vector determines the number of inputs a dense layer accepts.

Our layer needs two inputs, even if we collect a million more examples.

How basis vectors define a linear transformation

Before we get to the transformations performed by a layer, let’s see how a matrix describes a linear transformation through the destinations of the input basis vectors. Once we know where those vectors land, linearity tells us where every other input vector lands.

In two-dimensional space, we need two linearly independent vectors to form a basis. We use the usual unit vectors, one along each coordinate axis:

e1=[10],e2=[01]\mathbf{e}_1=\begin{bmatrix}1\\0\end{bmatrix}, \qquad \mathbf{e}_2=\begin{bmatrix}0\\1\end{bmatrix}

They form the usual basis of the input plane. Putting these two vectors into columns gives the identity matrix:

I=[e1  e2]=[1001]I=[\,\mathbf{e}_1\;\mathbf{e}_2\,] =\begin{bmatrix}1&0\\0&1\end{bmatrix}

The basis matrix makes it possible for us to reconstruct any vector from its coordinates in that basis. Suppose we define a vector as

x=[x1x2]\mathbf{x}=\begin{bmatrix}x_1\\x_2\end{bmatrix}

Its first coordinate, x1x_1, scales the first column, e1\mathbf{e}_1, and its second coordinate, x2x_2, scales the second column, e2\mathbf{e}_2. We now use a linear combination of the basis vectors, with x1x_1 and x2x_2 as coefficients, to reconstruct the original vector:

Ix=x1e1+x2e2linear combination of basis vectors=x1[10]+x2[01]=[x1x2]=xI\mathbf{x}=\underbrace{x_1\mathbf{e}_1+x_2\mathbf{e}_2}_{\text{linear combination of basis vectors}} =x_1\begin{bmatrix}1\\0\end{bmatrix} +x_2\begin{bmatrix}0\\1\end{bmatrix} =\begin{bmatrix}x_1\\x_2\end{bmatrix}=\mathbf{x}

This means that to define a linear transformation, we can choose new coordinates for where the basis vectors e1\mathbf{e}_1 and e2\mathbf{e}_2 land and put those destinations into the matrix’s columns. We then use the same linear-combination logic, keeping the input vector’s original coordinates as coefficients, to construct the transformed vector and see where it lands in the output space.

Suppose a linear transformation sends e1\mathbf{e}_1 to [a,b][a,b]^\top and e2\mathbf{e}_2 to [c,d][c,d]^\top. The transformation matrix records these new destinations as its columns:

M=[acbd],Me1=[ab],Me2=[cd]M=\begin{bmatrix}a&c\\b&d\end{bmatrix}, \qquad M\mathbf{e}_1=\begin{bmatrix}a\\b\end{bmatrix}, \qquad M\mathbf{e}_2=\begin{bmatrix}c\\d\end{bmatrix}

Just as we saw above, we can reconstruct the transformed input vector by taking a linear combination of these transformed directions, using its original coordinates as coefficients. We scale the first column of MM by x1x_1 and the second by x2x_2, then add the results:

M[x1x2]=x1[ab]+x2[cd]=[ax1+cx2bx1+dx2]M\begin{bmatrix}x_1\\x_2\end{bmatrix} =x_1\begin{bmatrix}a\\b\end{bmatrix} +x_2\begin{bmatrix}c\\d\end{bmatrix} =\begin{bmatrix}ax_1+cx_2\\bx_1+dx_2\end{bmatrix}

The input coordinates (x1,x2)(x_1,x_2) have become the output coordinates (ax1+cx2,  bx1+dx2)(ax_1+cx_2,\;bx_1+dx_2). Some linear transformations collapse the plane onto a line or a point. They are still valid, and neural networks can use such transformations to discard information while preserving distinctions useful for the task.

The same rule carries every point of a grid, including points outside our dataset. Next, we will apply it to the matrix of our hidden layer.

A layer’s matrix defines a transformation

Let’s first look at how the hidden layer produces a new representation for any input vector. Then we will choose concrete weights and follow apple A through the matrix multiplication, calculating where its vector lands.

The hidden layer’s weight matrix WW is our transformation matrix: it defines how vectors from the input data or a previous layer are linearly transformed. Its shape depends on both the number of inputs and the number of neurons: with dd inputs and mm neurons, WW has mm rows and dd columns.

Each neuron defines one row of weights, which calculates one output coordinate. As we saw in the basis-vector explanation, each column shows where one input basis vector lands across all those output coordinates. Adding a neuron therefore adds an output coordinate, not another input basis vector. We can associate that coordinate with an axis in the output space, but the neuron’s weight vector is not itself that output basis vector.

Our layer has two neurons and two inputs, so WW has two rows and two columns. Each row holds one neuron’s weight for color (x1x_1) and its weight for softness (x2x_2).

W=[10.80.81]W = \begin{bmatrix} 1 & -0.8 \\ -0.8 & 1 \end{bmatrix}

These weights are parameters the network would learn during training. Here, we have chosen them by hand so every step is exact and easy to inspect.

To apply this transformation, we multiply the incoming data vector x\mathbf{x} by WW, as WxW\mathbf{x}. This computes one weighted sum per neuron.

Alongside its weight matrix, our hidden layer also has a bias vector b\mathbf{b}, with one bias for each of its two neurons. Adding b\mathbf{b} gives z=Wx+b\mathbf{z}=W\mathbf{x}+\mathbf{b}, the pre-activation vector. Together, the multiplication and bias addition form an affine transformation: a linear transformation followed by a shift. The same bias vector shifts every result. A linear transformation always maps the origin to the origin; adding the bias moves it to b\mathbf{b}. In our example below, b=0\mathbf{b}=\mathbf{0}, so there is no shift and the map is also linear.

b=[00]\mathbf{b}=\begin{bmatrix}0\\0\end{bmatrix}

ReLU then replaces each negative coordinate of z\mathbf{z} with zero, producing h=(h1,h2)\mathbf{h}=(h_1,h_2), the hidden layer’s output. This is the apple’s new representation, which the output neuron will use to calculate its score. The complete sequence is:

x    W    Wx    +b    z    ReLU    h\mathbf{x} \;\xrightarrow{\;W\;}\; W\mathbf{x} \;\xrightarrow{\;+\mathbf{b}\;}\; \mathbf{z} \;\xrightarrow{\;\text{ReLU}\;}\; \mathbf{h}

Define the same transformation matrix and bias in NumPy:

# Two input coordinates -> two pre-activation coordinates.
# Rows hold neuron weights; columns record input basis-vector images.
W = np.array([[1.0, -0.8],
              [-0.8, 1.0]])     # Shape (2, 2)
b = np.array([0.0, 0.0])        # Bias vector; no shift here

We will use apple A as an example, but the widget below lets you follow any of the four apples through the same matrix WW. A’s original vector is

xA=[0.50.5]\mathbf{x}_A=\begin{bmatrix}-0.5\\-0.5\end{bmatrix}

Using the column-combination rule, we can calculate WxAW\mathbf{x}_A step by step from its original coordinates and the columns of WW.

  1. Read the columns of WW as destinations of the basis vectors e1\mathbf{e}_1 and e2\mathbf{e}_2. The first column tells us where e1=(1,0)\mathbf{e}_1=(1,0) lands; the second tells us where e2=(0,1)\mathbf{e}_2=(0,1) lands:

    W=[10.80.81],We1=[10.8],We2=[0.81].W=\begin{bmatrix}1&-0.8\\-0.8&1\end{bmatrix}, \qquad W\mathbf{e}_1=\begin{bmatrix}1\\-0.8\end{bmatrix}, \qquad W\mathbf{e}_2=\begin{bmatrix}-0.8\\1\end{bmatrix}.

    These are the original basis vectors after applying WW.

  2. Use A’s original coordinates as coefficients. As we showed above, we can use a linear combination of the transformed basis vectors to construct A’s vector in the transformed space:

    WxA=0.5(We1)+(0.5)(We2)W\mathbf{x}_A=-0.5(W\mathbf{e}_1)+(-0.5)(W\mathbf{e}_2)

    The coefficients stay the same; the vectors they multiply are now the transformed directions. The first coordinate, 0.5-0.5, scales the first column of WW, which is the transformed first basis vector, We1W\mathbf{e}_1. The second coordinate, also 0.5-0.5, scales the second column, the transformed second basis vector, We2W\mathbf{e}_2.

    So the full calculation looks like this:

    WxA=0.5[10.8]+(0.5)[0.81]=[(0.5)×1(0.5)×(0.8)]+[(0.5)×(0.8)(0.5)×1]=[0.50.4]+[0.40.5]=[0.5+0.40.4+(0.5)]=[0.10.1]\begin{aligned} W\mathbf{x}_A &=-0.5\begin{bmatrix}1\\-0.8\end{bmatrix} +(-0.5)\begin{bmatrix}-0.8\\1\end{bmatrix}\\[6pt] &=\begin{bmatrix}(-0.5)\times 1\\(-0.5)\times(-0.8)\end{bmatrix} +\begin{bmatrix}(-0.5)\times(-0.8)\\(-0.5)\times 1\end{bmatrix}\\[6pt] &=\begin{bmatrix}-0.5\\0.4\end{bmatrix} +\begin{bmatrix}0.4\\-0.5\end{bmatrix} =\begin{bmatrix}-0.5+0.4\\0.4+(-0.5)\end{bmatrix} =\begin{bmatrix}-0.1\\-0.1\end{bmatrix} \end{aligned}

The two contributions partially offset each other, leaving (0.1,0.1)(-0.1,-0.1). We used A’s original coordinates to combine the transformed basis directions. The same rule applies to B, whose coefficients are both +0.5+0.5:

WxB=0.5[10.8]+0.5[0.81]=[0.10.1]W\mathbf{x}_B =0.5\begin{bmatrix}1\\-0.8\end{bmatrix} +0.5\begin{bmatrix}-0.8\\1\end{bmatrix} =\begin{bmatrix}0.1\\0.1\end{bmatrix}

A and B move closer to the origin but remain different points. The matrix changes their representations without merging them.

The animation below shows both steps together: the basis arrows move to the destinations recorded in WW, and the selected apple follows their linear combination using its original coordinates as coefficients. The blue grid follows the same transformation, while the original grid stays fixed for reference.

Select A, B, C, or D, then choose Transform. With A selected initially, its vector moves from (0.5,0.5)(-0.5,-0.5) to (0.1,0.1)(-0.1,-0.1).

Apple:
-1-1110eeApple A: (−0.50, −0.50)A
Original: (−0.50, −0.50)Now: (−0.50, −0.50)

We can see what the transformation does by following the two diagonals:

W[11]=0.2[11],W[11]=1.8[11]W\begin{bmatrix}1\\1\end{bmatrix} =0.2\begin{bmatrix}1\\1\end{bmatrix}, \qquad W\begin{bmatrix}1\\-1\end{bmatrix} =1.8\begin{bmatrix}1\\-1\end{bmatrix}

The A–B direction shrinks to one fifth of its length, while the C–D direction stretches by a factor of 1.81.8. Neither direction disappears. This changes the shape of the four-point square but preserves the crossing of the class diagonals. The linear transformation alone has not made XOR separable. For that, we’ll need to add nonlinearity through the ReLU activation, which we will examine below.

Here’s how to implement this transformation in NumPy, using the matrix W defined above. The @ operator performs matrix multiplication:

x_A = np.array([-0.5, -0.5])    # A's original coordinates

# Apply W to the whole vector.
transformed_A = W @ x_A
print(transformed_A)             # [-0.1 -0.1]

For matrix–vector multiplication, W @ x_A is equivalent to scaling each column of W by the corresponding coordinate of x_A and adding the resulting vectors. We can write that linear combination explicitly:

# W[:, 0] is the first column; W[:, 1] is the second.
transformed_A = x_A[0] * W[:, 0] + x_A[1] * W[:, 1]
print(transformed_A)             # [-0.1 -0.1]

How neurons work together to transform a vector

Above, we used the layer’s transformation matrix to transform apple A’s input vector:

W=[10.80.81],xA=[0.50.5]WxA=[0.10.1]W=\begin{bmatrix}1&-0.8\\-0.8&1\end{bmatrix}, \qquad \mathbf{x}_A=\begin{bmatrix}-0.5\\-0.5\end{bmatrix} \quad\longmapsto\quad W\mathbf{x}_A=\begin{bmatrix}-0.1\\-0.1\end{bmatrix}

Now let’s look at how this matrix is constructed from the neurons in the layer and the dimensionality of each input vector, and where each neuron’s work appears in that calculation.

In our fully connected layer, every neuron receives the same complete input vector. Each apple has two input coordinates, so each neuron needs two weights: one for x1x_1 and one for x2x_2. We have two neurons, each supplying one row of weights:

NeuronWeight for x1x_1Weight for x2x_2Row of WW
1110.8-0.8(1,0.8)(1,-0.8)
20.8-0.811(0.8,1)(-0.8,1)

Each row holds one neuron’s weights, while each column records where one input basis vector lands. For example, the first column contains both neurons’ weights for x1x_1. For input e1=(1,0)\mathbf{e}_1=(1,0), those weights give the two output coordinates, We1=(1,0.8)W\mathbf{e}_1=(1,-0.8). So a column combines contributions from both neurons.

Stacking these rows gives our 2×22\times2 matrix WW. More generally, a dense layer with dd input coordinates and mm neurons has

WRm×d,xRd    WxRmW\in\mathbb{R}^{m\times d}, \qquad \mathbf{x}\in\mathbb{R}^d \;\longmapsto\; W\mathbf{x}\in\mathbb{R}^m

The input dimension determines the number of columns; the number of neurons determines the number of rows and output coordinates. These counts determine the matrix’s shape. The weight values determine which linear transformation it performs. For example, two neurons receiving three-dimensional inputs would give a 2×32\times3 matrix, mapping three input coordinates to two output coordinates.

Interestingly, taking a linear combination of the matrix columns, using the input coordinates as coefficients, and taking a dot product of each matrix row with the input vector give exactly the same transformed vector.

In the previous section, we obtained WxA=(0.1,0.1)W\mathbf{x}_A=(-0.1,-0.1) by scaling the columns by A’s original coordinates and adding them. Now we will obtain the same result through the neurons’ calculations: each neuron takes a dot product of its row of weights with the complete input vector, producing one output coordinate.

For fixed weights, that dot product defines a function that takes an input vector and returns one scalar number: fi(x)=wixf_i(\mathbf{x})=\mathbf{w}_i\cdot\mathbf{x}. For our first neuron, this scalar-valued linear function is f1(x)=x10.8x2f_1(\mathbf{x})=x_1-0.8x_2, mapping R2\mathbb{R}^2 to R\mathbb{R}. The transformation matrix, and the linear part of the layer it describes, can therefore be viewed as a collection of these functions, one per neuron. Together, their results form T(x)=(f1(x),f2(x))=WxT(\mathbf{x})=(f_1(\mathbf{x}),f_2(\mathbf{x}))^\top=W\mathbf{x}, mapping R2\mathbb{R}^2 to R2\mathbb{R}^2.

With our zero biases, the two functions’ results are also the pre-activation coordinates. Writing out their dot products gives

z=Wx=[w1xw2x]=[1x1+(0.8)x2(0.8)x1+1x2]\mathbf{z}=W\mathbf{x} =\begin{bmatrix} \mathbf{w}_1\cdot\mathbf{x}\\ \mathbf{w}_2\cdot\mathbf{x} \end{bmatrix} =\begin{bmatrix} 1\cdot x_1+(-0.8)\cdot x_2\\ (-0.8)\cdot x_1+1\cdot x_2 \end{bmatrix}

For the same apple A, both neurons receive (0.5,0.5)(-0.5,-0.5):

NeuronIts calculationCoordinate it produces
11(0.5)+(0.8)(0.5)=0.11(-0.5)+(-0.8)(-0.5)=-0.1z1=0.1z_1=-0.1
2(0.8)(0.5)+1(0.5)=0.1(-0.8)(-0.5)+1(-0.5)=-0.1z2=0.1z_2=-0.1

Collecting these two numbers gives zA=(0.1,0.1)\mathbf{z}_A=(-0.1,-0.1), exactly the vector we constructed by combining columns. The neurons’ weighted sums together perform the layer’s matrix multiplication. Each neuron contributes one coordinate of the result; it does not apply the whole matrix independently. The same weights are used for every apple.

The two views group the same arithmetic differently. A column collects one input coordinate’s contributions to all neurons. A row collects all the weights used by one neuron to calculate its output coordinate. Neither column belongs to a single neuron. Our chosen WW is symmetric, so its rows and columns happen to contain the same numbers, but their roles are different.

x1[10.8]+x2[0.81]column combination=[x10.8x20.8x1+x2]=[(1,0.8)(x1,x2)(0.8,1)(x1,x2)]row dot products\underbrace{ x_1\begin{bmatrix}1\\-0.8\end{bmatrix} +x_2\begin{bmatrix}-0.8\\1\end{bmatrix} }_{\text{column combination}} =\begin{bmatrix}x_1-0.8x_2\\-0.8x_1+x_2\end{bmatrix} =\underbrace{ \begin{bmatrix} (1,-0.8)\cdot(x_1,x_2)\\ (-0.8,1)\cdot(x_1,x_2) \end{bmatrix} }_{\text{row dot products}}

We can make the individual neurons’ calculations explicit in NumPy:

# Each row is one neuron's weight vector.
z1_A = np.dot(W[0], x_A)
z2_A = np.dot(W[1], x_A)
z_A = np.array([z1_A, z2_A])
print(z_A)                      # [-0.1 -0.1], the same as W @ x_A

Adding each neuron’s bias makes its function affine; applying ReLU then makes it nonlinear. Together, these outputs form the hidden representation h\mathbf{h}. We will examine the effect of ReLU below. To continue the forward-pass code for apple C, using its earlier input vector x, we compute both neurons’ weighted sums and add their biases in one operation:

z = W @ x + b
print(z)                        # [-0.9  0.9] for apple C

Now when we combine all input vectors into a representation matrix, we can apply the layer’s transformation to all four apples in one matrix multiplication. We put A, B, C, and D in the rows of XX, with their two input coordinates in the columns.

For one column vector, we used WxW\mathbf{x}. Because our batch stores vectors as rows, we use XWXW^\top: transposing WW puts each neuron’s weights into a column, so each apple’s row takes a dot product with each neuron’s weights.

The transpose follows our storage convention: examples are rows of XX, and neurons’ weights are rows of WW. If the weights were stored with one neuron per column instead, we would use XWXW directly. Transposing aligns the dimensions; it does not change the intended transformation.

With our zero biases, the calculation is

Z=XW=[0.50.50.50.50.50.50.50.5]×[10.80.81]=[0.10.10.10.10.90.90.90.9]Z=XW^\top =\begin{bmatrix} -0.5&-0.5\\ 0.5&0.5\\ -0.5&0.5\\ 0.5&-0.5 \end{bmatrix} \times \begin{bmatrix} 1&-0.8\\ -0.8&1 \end{bmatrix} =\begin{bmatrix} -0.1&-0.1\\ 0.1&0.1\\ -0.9&0.9\\ 0.9&-0.9 \end{bmatrix}

Each row of ZZ holds one apple’s transformed vector; each column holds one neuron’s weighted sums across the batch. The first row is still (0.1,0.1)(-0.1,-0.1) for A, exactly as when we processed it individually. Our chosen WW is symmetric, so WW^\top happens to have the same entries as WW, but the transpose expresses the correct operation for examples stored as rows.

In NumPy, using the same W and b defined above:

# One input vector per row, in A, B, C, D order.
X = np.array([
    [-0.5, -0.5],
    [ 0.5,  0.5],
    [-0.5,  0.5],
    [ 0.5, -0.5],
])

# (4 apples, 2 inputs) @ (2 inputs, 2 neurons) -> (4, 2)
# Each apple's row takes a dot product with each neuron's weights.
Z = X @ W.T + b                  # Add the same bias vector to every row
print(Z)
# [[-0.1 -0.1]   A
#  [ 0.1  0.1]   B
#  [-0.9  0.9]   C
#  [ 0.9 -0.9]]  D

Batching groups the calculations without changing the transformation of any individual vector. Every apple goes through both neurons using the same weights and biases; its measurements are never combined with another apple’s.

The activation makes the new representation separable

So far, we have calculated the linear part of the hidden layer: each neuron produces one coordinate of z=Wx\mathbf{z}=W\mathbf{x}, with no shift because our biases are zero. These are the coordinates before activation:

PointClassz=Wx\mathbf{z}=W\mathbf{x}
A0(0.1,0.1)(-0.1,-0.1)
B0(0.1,0.1)(0.1,0.1)
C1(0.9,0.9)(-0.9,0.9)
D1(0.9,0.9)(0.9,-0.9)

The A–B segment is shorter and the C–D segment is longer, but they still cross. A straight line cannot separate the two classes.

Before W
-1-111x₁x₂ABCD
After W
-1-111z₁z₂ABCD

Same scale in both plots. The segments join points of the same class and still cross at the origin.

The hidden layer has one more operation to perform. Each neuron applies the nonlinear ReLU activation to its own weighted sum. Together, these results form the hidden layer’s output vector, h\mathbf{h}:

ReLU(z)=max(0,z),h=ReLU(z)\operatorname{ReLU}(z)=\max(0,z), \qquad \mathbf{h}=\operatorname{ReLU}(\mathbf{z})

It keeps positive values and replaces negative values with zero. Our two hidden coordinates become

h1=max(0,x10.8x2),h2=max(0,x20.8x1)h_1=\max(0,x_1-0.8x_2), \qquad h_2=\max(0,x_2-0.8x_1)

h1h_1 measures the positive part of x10.8x2x_1-0.8x_2, while h2h_2 measures the positive part of x20.8x1x_2-0.8x_1. These are the two features the hidden layer computes.

To see the effect of ReLU on each apple’s representation, select A, B, C, or D, then choose Apply ReLU. The widget starts with the coordinates produced by WW. With A selected initially, its vector changes from (0.1,0.1)(-0.1,-0.1) to (0,0)(0,0). Selecting another apple resets the animation to that apple’s coordinates before activation.

Apple:
-1-1110z₁z₂A
Before ReLU: (−0.10, −0.10)Now: (−0.10, −0.10)

Here are the coordinates before and after ReLU for all four apples, matching the results shown in the widget:

PointClassBefore ReLU: z\mathbf{z}After ReLU: h\mathbf{h}
A0(0.1,0.1)(-0.1,-0.1)(0,0)(0,0)
B0(0.1,0.1)(0.1,0.1)(0.1,0.1)(0.1,0.1)
C1(0.9,0.9)(-0.9,0.9)(0,0.9)(0,0.9)
D1(0.9,0.9)(0.9,-0.9)(0.9,0)(0.9,0)

A moves to the origin because both its coordinates are negative. B stays at (0.1,0.1)(0.1,0.1) because both are positive. C and D each lose their negative coordinate and land on different positive axes. The class segments no longer cross.

In NumPy, apply ReLU to the batch matrix Z calculated above. The maximum operation compares each coordinate with zero and keeps the larger value. Each row of H holds one apple’s hidden representation:

# Keep positive coordinates; replace negative ones with zero.
H = np.maximum(0, Z)
print(H)
# [[0.  0. ]   A
#  [0.1 0.1]   B
#  [0.  0.9]   C
#  [0.9 0. ]]  D

ReLU does not restrict its output to 0 or 1. Here it retains values such as 0.10.1 and 0.90.9. The final classification threshold is the operation that returns only 0 or 1.

The output layer classifies the hidden representations

The hidden layer has produced a new vector for each apple. The output neuron receives its two coordinates, h1h_1 and h2h_2.

Two inputs, two hidden neurons, and one output neuronBoth inputs connect to both hidden neurons through W. Each adds its bias and applies ReLU to produce h1 or h2. The output neuron combines them through V, adds c, and predicts class 1 for a positive score, class 0 otherwise.Input vectorHidden layerOutput neuronWVx₁x₂h₁Σ + b₁ReLUh₂Σ + b₂ReLUΣ + cs > 0?Class: 0 or 1

This output neuron has its own weights, stored in VV, and bias, cc. Training would learn these parameters alongside the hidden layer’s weights and biases. For our demonstration, we choose them by hand:

V=[11],c=0.5V=\begin{bmatrix}1&1\end{bmatrix}, \qquad c=-0.5

Its score is another dot product, followed by the bias:

s(h)=Vh+c=[11]×[h1h2]+(0.5)=1h1+1h20.5=h1+h20.5s(\mathbf{h})=V\mathbf{h}+c =\begin{bmatrix}1&1\end{bmatrix} \times\begin{bmatrix}h_1\\h_2\end{bmatrix}+(-0.5) =1\cdot h_1+1\cdot h_2-0.5 =h_1+h_2-0.5

As before, a positive score predicts ripe (1); a zero or negative score predicts unripe (0). The decision boundary contains the representations whose score is zero:

h1+h20.5=0h1+h2=0.5h_1+h_2-0.5=0 \quad\Longrightarrow\quad h_1+h_2=0.5

This threshold falls between the sums for the two classes:

AppleHidden representation (h1,h2)(h_1,h_2)h1+h2h_1+h_2ScorePrediction
A(0,0)(0,0)000.5-0.5Unripe (0)
B(0.1,0.1)(0.1,0.1)0.20.20.3-0.3Unripe (0)
C(0,0.9)(0,0.9)0.90.90.40.4Ripe (1)
D(0.9,0)(0.9,0)0.90.90.40.4Ripe (1)

A and B have sums below 0.50.5, while C and D have sums above it. That is why the line h1+h2=0.5h_1+h_2=0.5 separates the classes. Our hand-chosen threshold is not unique: any value strictly between 0.20.2 and 0.90.9 would also separate these four representations.

Let’s look at how the complete network classifies our apples in the original input plane. The widget opens in Top view (2D), showing the decision regions with all four apples classified correctly at our hand-chosen parameter values. Switch to Angled view (3D) to see (x1,x2,s(x))(x_1,x_2,s(\mathbf{x})): each apple’s original coordinates together with its final score as height. This third axis visualizes the scalar output; the hidden representation still has two coordinates. The blue score surface has creases where the hidden neurons switch between zero and positive outputs, and its intersection with the input plane forms the decision boundary. Adjust the weight and bias sliders to explore how the surface and predictions change, or select Reset to restore our parameter values.

Adjust weights and biases
Hidden neuron 1: h1 = ReLU(1.00x₁ + (-0.80)x₂ + (0.00))
1.00
-0.80
0.00
Hidden neuron 2: h2 = ReLU(-0.80x₁ + (1.00)x₂ + (0.00))
-0.80
1.00
0.00
Output: s = 1.00h₁ + (1.00)h₂ + (-0.50)
1.00
1.00
-0.50

Adjust the hidden weights to change the folds, hidden biases to shift them, and output weights to change their contribution to the score. The output bias raises or lowers the whole surface. Reset restores the article’s values.

s(x) = Σⱼ vⱼ ReLU(wⱼ₁x₁ + wⱼ₂x₂ + bⱼ) + c

x₁-11x₂-11Apple A: Color -0.5, Softness -0.5, Score -0.50, Prediction 0Apple B: Color 0.5, Softness 0.5, Score -0.30, Prediction 0Apple C: Color -0.5, Softness 0.5, Score 0.40, Prediction 1Apple D: Color 0.5, Softness -0.5, Score 0.40, Prediction 1ABCD

The boundary is straight in hidden space, but bends in the original input space. A and B lie in the unripe region; C and D lie in the ripe region on opposite sides of it. With these weights, the two ripe sides connect farther along the positive diagonal, beyond the four example points. Correctly classifying four examples does not specify what the network should predict everywhere else.

To implement the output layer in NumPy, we apply its weights and bias to all four hidden representations. With one apple per row of HH, we compute S=HV+cS=HV^\top+c, adding the same bias to every apple’s score:

# Output transformation: two hidden coordinates -> one score.
V = np.array([[1.0, 1.0]])       # Shape (1, 2)
c = -0.5                        # Output bias

# (4 apples, 2 hidden coordinates) @ (2, 1) -> (4, 1)
# Each row takes a dot product with the same output weights.
S = H @ V.T + c
predictions = (S > 0).astype(int)  # 1 = ripe, 0 = unripe

for apple, score, prediction in zip("ABCD", S[:, 0], predictions[:, 0]):
    print(f"{apple}: score={score:.1f}, prediction={prediction}")
# A: score=-0.5, prediction=0
# B: score=-0.3, prediction=0
# C: score=0.4, prediction=1
# D: score=0.4, prediction=1

Why ReLU alone does not solve this dataset

What if the hidden layer used the identity matrix II for its weights, with zero biases? It would compute h=ReLU(Ix)=ReLU(x)\mathbf{h}=\operatorname{ReLU}(I\mathbf{x})=\operatorname{ReLU}(\mathbf{x}), exactly the same as applying ReLU directly to the original inputs. The result would still be an XOR arrangement: negative input coordinates become zero, leaving A at (0,0)(0,0), B at (0.5,0.5)(0.5,0.5), C at (0,0.5)(0,0.5), and D at (0.5,0)(0.5,0). These are the four corners of a smaller square, with each class still occupying a different diagonal. No straight line separates them.

Switch between Original, ReLU(x), and ReLU(Wx) to compare the three arrangements. The second view uses identity weights; the third uses our chosen WW. Both hidden-layer alternatives start from the original input vectors and use zero biases.

Original inputs: the two class diagonals cross.

-1-1-0.5-0.500.50.511x₁x₂A: (−0.5, −0.5), Label 0AB: (0.5, 0.5), Label 0BC: (−0.5, 0.5), Label 1CD: (0.5, −0.5), Label 1D

Each square is 0.1 × 0.1. The axes use the same scale in all three views.

A: (−0.5, −0.5)B: (0.5, 0.5)C: (−0.5, 0.5)D: (0.5, −0.5)

No straight line separates the two classes.

The matrix WW first changes the coordinates so that ReLU acts on a more useful arrangement. It places B at (0.1,0.1)(0.1,0.1) and C and D at (0.9,0.9)(-0.9,0.9) and (0.9,0.9)(0.9,-0.9). ReLU then keeps B near the origin and places C and D farther along the positive axes, while A lands at the origin. The resulting representations can be separated by a straight line.

Neither WW alone nor ReLU applied directly to these inputs solves this example; their combination makes the classes linearly separable. The output layer can separate the resulting representations, as we saw above.

Layers compose functions

Some books describe neural networks as a composition of functions that map vectors from one vector space to another. Our hidden layer maps R2\mathbb{R}^2 to R2\mathbb{R}^2, producing a hidden representation, and our output layer maps R2\mathbb{R}^2 to R\mathbb{R}, producing one scalar score. Each function receives the result of the previous one. These functions can be linear or nonlinear, as we saw with the weight matrix and ReLU.

For our network, call the hidden-layer function FF and the output-layer function GG:

F(x)=ReLU(Wx+b),G(h)=Vh+cF(\mathbf{x})=\operatorname{ReLU}(W\mathbf{x}+\mathbf{b}), \qquad G(\mathbf{h})=V\mathbf{h}+c

The forward pass we have followed through the widgets is therefore

xFhGs(x)\mathbf{x}\xrightarrow{F}\mathbf{h}\xrightarrow{G}s(\mathbf{x})

Substituting the hidden layer’s output into the output-layer function gives

s(x)=G(F(x))=V×ReLU(Wx+b)+cs(\mathbf{x})=G(F(\mathbf{x})) =V\times\operatorname{ReLU}(W\mathbf{x}+\mathbf{b})+c

This is what function composition means here. The common notation for this composition is GFG\circ F, meaning apply FF first, then GG. A deeper network follows the same pattern, FLF2F1F_L\circ\cdots\circ F_2\circ F_1: each layer transforms the representation it receives and passes its result to the next.

Without nonlinear activations, the layers combine into one affine map

You may also have read that without nonlinear activations, all the layers collapse into one. This is easy to see with our two weight matrices. Temporarily remove ReLU. Because our hidden biases are zero, the output neuron now receives WxW\mathbf{x} directly:

swithout ReLU(x)=V(Wx)+c=(VW)x+cs_{\text{without ReLU}}(\mathbf{x}) =V(W\mathbf{x})+c =(VW)\mathbf{x}+c

We can multiply VV and WW first and use their product as a single weight matrix:

VW=[11]×[10.80.81]=[11+1(0.8)1(0.8)+11]=[0.20.2]VW= \begin{bmatrix}1&1\end{bmatrix} \times\begin{bmatrix}1&-0.8\\-0.8&1\end{bmatrix} =\begin{bmatrix}1\cdot1+1\cdot(-0.8)&1\cdot(-0.8)+1\cdot1\end{bmatrix} =\begin{bmatrix}0.2&0.2\end{bmatrix}

The resulting network computes

swithout ReLU(x)=0.2x1+0.2x20.5s_{\text{without ReLU}}(\mathbf{x})=0.2x_1+0.2x_2-0.5

We are back to the same kind of score as our original single neuron: two weighted inputs plus a bias, with a straight-line decision boundary. These particular weights give scores 0.7-0.7, 0.3-0.3, 0.5-0.5, and 0.5-0.5 for A, B, C, and D, predicting unripe for all four. Changing the weights could improve those predictions, but no single affine score can classify this XOR arrangement correctly.

The word “collapse” refers to the function the network computes: a single affine layer can produce exactly the same outputs as the whole stack. Nonzero biases do not change this conclusion. They combine into one bias:

V(Wx+b)+c=(VW)x+(Vb+c)V(W\mathbf{x}+\mathbf{b})+c =(VW)\mathbf{x}+(V\mathbf{b}+c)

Repeating this calculation combines any stack of affine layers into one affine map. Applying our final step threshold still produces a straight-line boundary; the threshold alone cannot give us the effect of a nonlinear hidden layer.

Matrix multiplication records the composed transformation

This connects directly to our earlier view of columns as destinations of the input basis vectors. The columns of WW tell us where e1\mathbf{e}_1 and e2\mathbf{e}_2 land after the first transformation. Applying VV to those destinations tells us where they land after both:

VW=[V(We1)V(We2)]=[0.20.2]VW=\begin{bmatrix}V(W\mathbf{e}_1)&V(W\mathbf{e}_2)\end{bmatrix} =\begin{bmatrix}0.2&0.2\end{bmatrix}

Each input basis vector ends up at 0.20.2 on the output number line. For any input, the same column-combination rule therefore gives (VW)x=0.2x1+0.2x2(VW)\mathbf{x}=0.2x_1+0.2x_2.

More generally, multiplying two transformation matrices applies the left matrix to every column of the right matrix. If we apply M1M_1 and then M2M_2, their product records the final destinations of the input basis vectors:

xM1M1xM2M2(M1x)=(M2M1)x\mathbf{x}\xrightarrow{M_1}M_1\mathbf{x} \xrightarrow{M_2}M_2(M_1\mathbf{x}) =(M_2M_1)\mathbf{x}

The rightmost matrix acts first. The product represents both linear transformations as one.

ReLU changes what the next layer receives

Now put ReLU back between WW and VV. The output neuron receives ReLU(Wx)\operatorname{ReLU}(W\mathbf{x}), so it no longer simply adds the two coordinates produced by WW. It adds their nonnegative parts:

s(x)=max(0,x10.8x2)+max(0,x20.8x1)0.5s(\mathbf{x}) =\max(0,x_1-0.8x_2)+\max(0,x_2-0.8x_1)-0.5

For example, C’s coordinates after WW are (0.9,0.9)(-0.9,0.9). Without ReLU, they cancel and give a score of 0.5-0.5. With ReLU, the output neuron receives (0,0.9)(0,0.9) and gives a score of 0.40.4. The same output weights now produce the correct prediction because they receive a different representation.

VReLU(Wx)(VW)xin generalV\operatorname{ReLU}(W\mathbf{x})\neq(VW)\mathbf{x} \quad\text{in general}

The layers still compose as functions, but we cannot replace this network with one affine layer. ReLU keeps or removes each coordinate according to its sign, making the complete score a nonlinear function of the original input.

This also connects the two views in our classifier widget. In hidden space, the output boundary is the straight line h1+h2=0.5h_1+h_2=0.5. Substituting the hidden-layer functions gives the same boundary in the original input coordinates:

max(0,x10.8x2)+max(0,x20.8x1)=0.5\max(0,x_1-0.8x_2)+\max(0,x_2-0.8x_1)=0.5

Where only the first neuron is active, this reduces to x10.8x2=0.5x_1-0.8x_2=0.5. Where only the second is active, it becomes x20.8x1=0.5x_2-0.8x_1=0.5. Where both are active, it becomes 0.2(x1+x2)=0.50.2(x_1+x_2)=0.5. These straight pieces join at corners, producing the boundary we see in the original input plane.

Every new apple goes through this same composition: its measurements become a hidden representation, which becomes a score and then a predicted label. A straight boundary in hidden space can therefore correspond to a nonlinear boundary in the original space.

Run the model on all four apples with NumPy

Let’s now bring the operations together in one runnable NumPy example. Each row remains one apple throughout the forward pass:

X4×2    Z4×2    ReLU    H4×2    S4×1\underbrace{X}_{4\times2} \;\longrightarrow\; \underbrace{Z}_{4\times2} \;\xrightarrow{\;\operatorname{ReLU}\;}\; \underbrace{H}_{4\times2} \;\longrightarrow\; \underbrace{S}_{4\times1}

We use the same row-per-example convention as above: Z=XW+bZ=XW^\top+\mathbf{b}, H=ReLU(Z)H=\operatorname{ReLU}(Z), and S=HV+cS=HV^\top+c, with each bias added to every row. Batch size and representation size are separate dimensions: the four examples stay the same, while each apple’s two hidden coordinates become one output score.

import numpy as np

# Representation matrix X, shape (4, 2):
# rows = apples A, B, C, D; columns = color and softness.
X = np.array([
    [-0.5, -0.5],
    [ 0.5,  0.5],
    [-0.5,  0.5],
    [ 0.5, -0.5],
])
labels = np.array([0, 0, 1, 1])   # Targets, kept separate from X

# Transformation matrix W, shape (2, 2), shared by all apples.
# Rows hold neuron weights; columns are transformed basis vectors.
W = np.array([
    [ 1.0, -0.8],
    [-0.8,  1.0],
])
b = np.array([0.0, 0.0])

# Output transformation V, shape (1, 2): two coordinates -> one score.
V = np.array([[1.0, 1.0]])
c = -0.5

# The examples are rows, so transpose W to compute each
# apple's dot product with each neuron's weights: (4, 2) @ (2, 2).
# NumPy adds the same bias vector to every row.
Z = X @ W.T + b                  # Shape (4, 2)

# ReLU replaces negative coordinates with zero.
H = np.maximum(0, Z)             # Shape (4, 2): new representations

# Compose the output map with the hidden map, row by row:
# (4, 2) @ (2, 1) -> (4, 1), then add the same scalar bias.
scores = H @ V.T + c             # One score per apple
predictions = (scores[:, 0] > 0).astype(int)

print(H)
# [[0.  0. ]   A
#  [0.1 0.1]   B
#  [0.  0.9]   C
#  [0.9 0. ]]  D

print(scores[:, 0])             # [-0.5 -0.3  0.4  0.4]
print(predictions)              # [0 0 1 1]

XX and HH describe the same examples in different spaces. WW and VV specify transformations between those spaces. The activation between them is what lets the complete network express a nonlinear rule.

Why changing the representation helps

We followed four apples through a network, but the purpose was never simply to move points around. It was to construct coordinates in which the distinction we wanted to make became easier to express.

XOR makes this concrete. In the original color–softness plane, no straight line separates the classes. The weight matrix reshapes that arrangement, and ReLU changes it nonlinearly. In the resulting representation, a weighted sum and a threshold are enough. The classification problem has stayed the same; the representation used to solve it has changed.

This gives us a small example of how a network can express a rule through geometry and computation. The hidden vectors encode derived properties of the inputs, and the output neuron combines those properties to make a decision. The rule emerges from the complete sequence of operations.

Larger networks can learn representations with many more coordinates, encoding properties and relationships useful for their tasks. Their architecture determines the available dimensions; training adjusts how inputs are represented within them. Concepts need not correspond to individual coordinates: information can be distributed across many coordinates and used by later layers. Networks can also represent more features than they have dimensions by encoding them in overlapping directions, a phenomenon called superposition.

Our hand-chosen network lets us inspect how useful features are computed and used. Linear algebra supplies the transformations, nonlinear activations expand what they can accomplish, and training searches the space of possible weights and biases for a combination that minimizes the loss. Each combination defines a candidate input-to-output function.