The Artificial Neuron

From weighted inputs to neural-network layers

The problem

To estimate whether a household is likely to take up the outreach programme, a model may use several pieces of information at once. Travel time, the number of previous contacts, and whether information was received through a local group might each be useful, but they need not carry the same weight in the estimate. We need a small calculation that can combine them.

An artificial neuron is that calculation. It multiplies each input by a number that the model learns, adds the results, and adjusts the total by one further number. The result is passed to the next part of the model. Chapter 4 showed how the learning algorithm can work backwards through a chain of calculations; this chapter opens one link in that chain.

The running calculation

Suppose that the three pieces of information for one household have already been prepared as numbers:

\[ x=(x_1,x_2,x_3)=(2,4,1). \]

These are not raw measures such as minutes or visits. They are standardised inputs, so that the calculation can handle features recorded on different scales. To begin, give them three weights:

\[ w=(0.5,-1,2) \]

and one additional number, called the bias, \(b=3\). For the moment, treat these as the settings of a calculation. Training is the process that chooses settings that work well across many earlier households.

Level 1 - Intuition

The diagram is an arithmetic recipe. Each incoming value is multiplied by the number written beside its arrow. The three products are added, the bias is included, and the total becomes the neuron’s output.

Inputs do not contribute equally

The weights allow the calculation to treat the inputs differently. A positive weight means that a larger input raises this neuron’s score; a negative weight means that it lowers the score. A weight close to zero leaves little trace in this particular calculation. The signs and sizes are not chosen by hand in a trained network: they are adjusted from earlier examples.

Code
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyArrowPatch

plt.style.use("seaborn-v0_8-whitegrid")
blue, teal, orange, purple = "#24527a", "#3a9188", "#d97745", "#7655a6"

fig, ax = plt.subplots(figsize=(8.5, 4.2))
input_y = [0.78, 0.50, 0.22]
input_labels = ["x₁ = 2", "x₂ = 4", "x₃ = 1"]
weight_labels = ["w₁ = 0.5", "w₂ = -1", "w₃ = 2"]
weight_widths = [1.5, 2.5, 3.5]

for ypos, label, weight_label, width in zip(input_y, input_labels, weight_labels, weight_widths):
    ax.text(0.12, ypos, label, ha="center", va="center", color="white", weight="bold",
            bbox={"boxstyle": "round,pad=0.45", "facecolor": blue})
    ax.add_patch(FancyArrowPatch((0.23, ypos), (0.43, 0.5), arrowstyle="->",
                                 mutation_scale=14, linewidth=width, color=teal))
    ax.text(0.31, ypos + 0.06 if ypos != 0.5 else ypos + 0.09, weight_label,
            ha="center", color=teal)

ax.add_patch(Circle((0.52, 0.5), 0.10, facecolor=orange, alpha=0.95))
ax.text(0.52, 0.5, "weighted\nsum + bias", ha="center", va="center", color="white", weight="bold")
ax.add_patch(FancyArrowPatch((0.63, 0.5), (0.81, 0.5), arrowstyle="->",
                             mutation_scale=14, linewidth=2, color=orange))
ax.text(0.88, 0.5, "output", ha="center", va="center", color="white", weight="bold",
        bbox={"boxstyle": "round,pad=0.45", "facecolor": blue})
ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis("off")
plt.tight_layout()
plt.show()
Three input features with differently weighted arrows entering one neuron and producing an output.
Figure 6.1: A neuron weights several inputs, combines them, and produces one output.

The arrows encode the weights. The labels show why the three inputs do not have the same effect on the total: the second arrow has a negative weight, while the third has the largest positive weight.

Artificial neuron. A small calculation that gives each input a learned weight, adds the weighted inputs and a bias, and produces one value. An activation function may then transform that value before it is passed forward.

The bias sets a baseline

The bias is the part of the calculation that remains when all three inputs are zero. It moves the whole score up or down before any activation function is applied. Without it, the calculation would be forced through a particular starting point; with it, the model can learn where the score should sit before the inputs make their contributions.

This is easiest to see after the arithmetic has been laid out. The three weighted contributions in the running example total \(-1\). Adding the bias moves the total to \(2\). The bias is therefore not an optional decoration: it is one of the quantities learned during training.

Following the contributions

The arithmetic also explains why the largest input does not necessarily have the largest effect. Each input must be considered together with its weight.

Code
contribution_labels=["w₁x₁","w₂x₂","w₃x₃","bias"]
contribution_values=np.array([1.,-4.,2.,3.])
fig,ax=plt.subplots(figsize=(7.7,4.1))
bar_colours=[teal,orange,teal,blue]
ax.bar(contribution_labels,contribution_values,color=bar_colours)
for i,value in enumerate(contribution_values): ax.text(i,value+(.18 if value>=0 else -.34),f"{value:.0f}",ha="center",va="bottom" if value>=0 else "top")
ax.axhline(0,color="#777777",linewidth=.8); ax.text(3.65,1.85,"sum = 2",color=purple,fontsize=11)
ax.set(ylabel="Contribution to pre-activation",title="Signed contributions form the neuron score")
plt.tight_layout(); plt.show()
A bar chart shows the three signed weighted contributions from the running example and the positive bias, followed by their summed pre-activation.
Figure 6.2: The neuron’s pre-activation is the sum of signed weighted input contributions plus the bias; a large input does not necessarily make the largest contribution.

The second input is numerically largest, but its negative weight makes its contribution \(-4\). The first input contributes \(1\), the third contributes \(2\), and the bias contributes \(3\). Together they form the score \(2\).

One neuron produces one number in this way. The next level follows the calculation line by line, then shows how the same rule is applied to many households at once.

Level 2 - Mechanics

Working through one neuron

Take the three inputs from the running example. First multiply each one by its corresponding weight. The table keeps the products separate before they are added:

Input Value \(x_j\) Weight \(w_j\) Contribution \(w_jx_j\)
\(x_1\) 2 0.5 1
\(x_2\) 4 -1 -4
\(x_3\) 1 2 2

They sum to \(1-4+2=-1\). Adding the bias gives:

\[ z=-1+3=2. \]

The number 2 is called the pre-activation, written \(z\). It is the score before the neuron applies an activation function. If the activation is written \(g\), the value passed onward is \(a=g(z)\).

How the bias changes a boundary

For two inputs and no activation, values with the same pre-activation satisfy:

\[ w_1x_1+w_2x_2+b=c. \]

For a binary threshold at \(z=0\), the decision boundary is \(w_1x_1+w_2x_2+b=0\). Changing \(b\) moves the boundary without changing its slope.

Code
import numpy as np

x1 = np.linspace(-4, 4, 200)
w1, w2 = 1.0, -1.0
biases = [-2, 0, 2]
colours = [blue, teal, orange]

fig, ax = plt.subplots(figsize=(7.5, 4.5))
for bias, colour in zip(biases, colours):
    x2 = -(w1 * x1 + bias) / w2
    ax.plot(x1, x2, color=colour, linewidth=2.4, label=f"bias = {bias}")
ax.set(xlabel="Input x₁", ylabel="Input x₂", title="The bias shifts the boundary")
ax.set_xlim(-4, 4); ax.set_ylim(-4, 4)
ax.legend(frameon=False)
plt.tight_layout()
plt.show()
Three parallel decision boundaries corresponding to three bias values.
Figure 6.3: Changing the bias shifts a linear decision boundary while leaving its orientation unchanged.

The three lines are parallel because the weights are unchanged. Their positions differ because the bias changes the score assigned to every point. This is why the bias is a learned parameter rather than a cosmetic addition.

The activation changes what is passed forward

If the neuron uses the rectified linear unit, or ReLU,

\[ g(z)=\max(0,z), \]

the running example produces \(a=2\). A pre-activation of \(-2\) would produce \(a=0\). Different activation functions shape this hand-off in different ways; Chapter 7 examines that choice.

A neuron during training

The forward pass calculates \(z\) and \(a\). The loss compares the eventual network output with the target. Backpropagation calculates gradients for the weight and bias, and the optimiser updates them. The inputs are data; the weights and bias are parameters. The activation function usually has a fixed form, although some architectures use activations with learned parameters.

The calculation is now complete for one household. The mathematics and code below express the same calculation compactly and then apply it to several households without changing the learned settings.

Level 3 - Mathematics & Code

The mechanics can now be written compactly. This level introduces each symbol, checks the arithmetic manually, implements the same operation in PyTorch, and shows why nonlinearity is required between layers.

Vector form

Let \(x\in\mathbb{R}^p\) contain \(p\) input features, \(w\in\mathbb{R}^p\) contain one weight for each feature, and \(b\in\mathbb{R}\) be a scalar bias. The neuron computes:

\[ z=w^\top x+b=\sum_{j=1}^{p}w_jx_j+b, \]

followed by:

\[ a=g(z). \]

The transpose in \(w^\top x\) turns the two vectors into a dot product. Each pair \(w_jx_j\) produces one contribution; the sum and bias produce one scalar \(z\); the activation converts that scalar into the value sent to the next layer.

For the running example:

\[ z=0.5(2)+(-1)(4)+2(1)+3=2. \]

Code
import torch

x = torch.tensor([2.0, 4.0, 1.0])
w = torch.tensor([0.5, -1.0, 2.0])
b = torch.tensor(3.0)

contributions = w * x
z = torch.dot(w, x) + b
a = torch.relu(z)

print("weighted contributions:", contributions)
print(f"pre-activation z: {z.item():.1f}")
print(f"ReLU activation a: {a.item():.1f}")
weighted contributions: tensor([ 1., -4.,  2.])
pre-activation z: 2.0
ReLU activation a: 2.0

The code exposes the same stages as the table in Level 2. Elementwise multiplication produces the three contributions. torch.dot sums them. Adding b produces the pre-activation, and torch.relu applies the activation.

Manual parameters and nn.Linear

PyTorch’s Linear layer implements the affine transformation. Its stored weight uses shape (out_features, in_features), so a one-output layer with three inputs has a weight matrix of shape (1, 3) and one bias.

Code
import torch.nn as nn

layer = nn.Linear(3, 1)
with torch.no_grad():
    layer.weight.copy_(w.reshape(1, 3))
    layer.bias.copy_(b.reshape(1))

layer_output = layer(x)
print("weight shape:", tuple(layer.weight.shape))
print("bias shape:", tuple(layer.bias.shape))
print(f"layer output: {layer_output.item():.1f}")
weight shape: (1, 3)
bias shape: (1,)
layer output: 2.0

The result equals the manual pre-activation because both calculations use the same parameters. nn.Linear does not apply ReLU automatically. Keeping the affine transformation and activation separate allows architectures to choose different activation functions.

The stored orientation is worth checking because it prevents a common shape error. A one-neuron layer stores one row of weights, so layer.weight has shape \((1,3)\). A batch with shape \((B,3)\) returns an affine output with shape \((B,1)\). The extra final axis represents one output feature per observation; it should not be removed accidentally when later code expects a feature dimension.

Applying one neuron to a batch

The running calculation used one input vector and produced one output. In code, the same weight vector and bias are applied to every row in a batch. A batch of shape \(B\times p\) therefore produces one pre-activation and one activation for each of its \(B\) observations; the number of learned parameters does not grow with the batch.

Code
batch=np.array([[2.,4.,1.],[0.,1.,3.],[-1.,2.,2.]])
weights=np.array([.5,-1.,2.]); bias=3.
batch_z=batch@weights+bias
batch_a=np.maximum(0,batch_z)
print("input batch shape:",batch.shape)
print("shared weight and bias shapes:",weights.shape,np.asarray(bias).shape)
print("pre-activations:",batch_z)
print("ReLU activations:",batch_a)
input batch shape: (3, 3)
shared weight and bias shapes: (3,) ()
pre-activations: [2.  8.  4.5]
ReLU activations: [2.  8.  4.5]

The three outputs belong to three observations, not to three different neurons. Chapter 6 adds multiple learned weight vectors to produce several outputs for each observation.

Activation functions

Code
z_values = torch.linspace(-5, 5, 400)
activations = {
    "Sigmoid": torch.sigmoid(z_values),
    "Tanh": torch.tanh(z_values),
    "ReLU": torch.relu(z_values),
    "GELU": torch.nn.functional.gelu(z_values),
}

fig, axes = plt.subplots(2, 2, figsize=(9, 6), sharex=True)
for ax, (name, values), colour in zip(axes.flat, activations.items(), [blue, teal, orange, blue]):
    ax.plot(z_values.numpy(), values.numpy(), color=colour, linewidth=2.4)
    ax.axhline(0, color="#777777", linewidth=0.7)
    ax.axvline(0, color="#777777", linewidth=0.7)
    ax.set_title(name)
    ax.set_ylabel("Activation")
for ax in axes[1]:
    ax.set_xlabel("Pre-activation z")
plt.tight_layout()
plt.show()
Plots of sigmoid, tanh, ReLU, and GELU across pre-activation values from minus five to five.
Figure 6.4: Activation functions transform the pre-activation in different ways.

Sigmoid maps values to \((0,1)\) and is useful when an output is interpreted as a binary probability. Tanh maps to \((-1,1)\). ReLU sets negative values to zero and leaves positive values unchanged. GELU changes values smoothly according to their magnitude and is widely used in Transformer feed-forward blocks (Hendrycks and Gimpel 2016). The choice affects representation, gradients, and numerical behaviour.

Why stacked linear layers are still linear

Suppose two layers omit their activations:

\[ h=W_1x+b_1, \]

\[ y=W_2h+b_2. \]

Substituting the first equation into the second gives:

\[ y=W_2W_1x+(W_2b_1+b_2). \]

This has the same affine form as one layer. Depth alone therefore does not create nonlinear representational capacity. The activation between layers prevents the composition from collapsing into one affine transformation.

The argument concerns the function represented by the composition, not the usefulness of intermediate dimensions for every computational purpose. Two affine operations may be implemented separately for engineering or parameter-sharing reasons, but they do not introduce an input-dependent bend in the input-output mapping until a nonlinearity intervenes. Chapter 7 examines those bends and their derivatives directly.

Level 4 - Research & Systems

Chapter 6 develops the move from one neuron to a layer. Here the question is different: what changes when the same weighted-sum calculation is executed at the scale of contemporary models? The relevant constraints are matrix shapes, stored activations, memory traffic, numerical precision, and the need to initialise many units differently.

Feed-forward blocks in Transformers

A Transformer feed-forward block applies two learned projections with an activation between them (Vaswani et al. 2017):

\[ \operatorname{FFN}(x)=W_2g(W_1x+b_1)+b_2. \]

The first projection usually expands the hidden dimension; the second projects it back. If the hidden width is 4,096 and the intermediate width is 16,384, the first weight matrix alone contains \(4{,}096\times16{,}384=67{,}108{,}864\) parameters. The operation is conceptually a layer of many artificial neurons, but its systems behaviour is governed by matrix shapes, memory traffic, and accelerator kernels.

Code
fig, ax = plt.subplots(figsize=(9, 2.5))
blocks = [
    (0.12, "hidden\n4,096", blue),
    (0.37, "linear expand\n16,384", teal),
    (0.62, "GELU\n16,384", orange),
    (0.87, "linear project\n4,096", blue),
]
for xpos, label, colour in blocks:
    ax.text(xpos, 0.55, label, ha="center", va="center", color="white", weight="bold",
            bbox={"boxstyle": "round,pad=0.5", "facecolor": colour})
for left, right in zip(blocks[:-1], blocks[1:]):
    ax.add_patch(FancyArrowPatch((left[0] + 0.09, 0.55), (right[0] - 0.09, 0.55),
                                 arrowstyle="->", mutation_scale=14, color="#666666"))
ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis("off")
plt.tight_layout()
plt.show()
A flow diagram from hidden representation through expansion, GELU activation, and projection back to the hidden width.
Figure 6.5: A Transformer feed-forward block expands, activates, and projects each token representation.

The figure represents one token position, but implementations process many token positions and sequences together. The same weight matrices are applied across those positions. This reuse distinguishes the feed-forward block from attention, which mixes information across token positions.

Initialisation, scale, and gradient flow

Weights cannot all start at the same value because units would receive identical gradients and remain symmetric. They are usually sampled from distributions whose scale depends on layer width. Poorly scaled initialisation can make activations and gradients grow, vanish, or saturate as they pass through many layers. Xavier/Glorot initialisation was developed to control this behaviour for particular activation assumptions (Glorot and Bengio 2010).

Training then changes the weights using the gradients developed in Chapter 3 and Chapter 4. The neuron equation remains the forward computation; backpropagation supplies \(\partial L/\partial W\) and \(\partial L/\partial b\) for the whole layer.

Symmetry explains why identical initial weights are unsuitable for a layer with several units. If two units begin with identical weights and biases and receive the same inputs, they produce identical outputs and receive identical gradients under the same downstream path. Updating them preserves the duplication. Random initialisation gives the units different starting directions; the training signal can then differentiate their roles if the data and objective support it.

Hardware and numerical precision

Large linear layers are often limited by the rate at which hardware can move weights and activations as well as by arithmetic throughput. Lower-precision formats reduce memory and can increase matrix-multiplication throughput, but training must retain sufficient numerical range for gradients and optimiser state (Micikevicius et al. 2018). Kernel fusion can reduce the movement of intermediate values by combining bias addition and activation with the matrix multiplication.

These systems choices do not alter the definition of a neuron. They determine how millions of neuron-equivalent operations are represented and executed efficiently.

The same distinction applies to parameter precision. A model may store some values in lower precision for throughput while retaining selected accumulators or optimiser state at higher precision. Reporting only that a network uses an activation or a linear layer says little about the numerical contract under which it was trained. Chapter 8 assembles these choices into a training loop; Chapters 11-15 examine their optimisation and systems consequences.

Common misconceptions

  • An artificial neuron behaves like a biological neuron. The terminology is historical; the artificial neuron is a parameterised mathematical operation.
  • A neuron makes an intelligent decision. It calculates a weighted sum, adds a bias, and applies an activation.
  • A weight is automatically a causal effect or feature importance. It is a fitted parameter whose meaning depends on scaling, other parameters, and the model architecture.
  • The bias is optional in principle and therefore unimportant. Some architectures omit biases, but when present the bias changes the affine transformation and is trained with the weights.
  • nn.Linear includes an activation. It performs the affine transformation only; the activation is a separate operation.
  • Stacking linear layers creates a nonlinear model. Without activations, their composition is another affine transformation.
  • Transformers simulate neurons individually. Implementations use batched tensor and matrix operations.

Exercises

  1. For \(x=(3,2)\), \(w=(2,-1)\), and \(b=1\), calculate the weighted contributions and pre-activation.
  2. Apply ReLU to the result of Exercise 1.
  3. Explain how changing the bias affects a two-dimensional linear decision boundary.
  4. Why should a fitted weight not automatically be interpreted as a causal effect?
  5. Write the tensor shapes for a batch of 64 observations, 128 input features, and 512 output units.
  6. Calculate the number of parameters in the layer from Exercise 5, including biases.
  7. Implement the running example with nn.Linear and verify that it matches the manual calculation.
  8. Explain why two linear layers without an activation can be replaced by one linear layer.
  9. Compare ReLU and GELU using the activation plot. What happens for negative pre-activations?
  10. A feed-forward block expands width 1,024 to 4,096 and projects it back. Calculate the number of weight parameters, excluding biases.
  11. Why can activation and weight scale affect gradient flow?
  12. Explain why a GPU implementation uses matrix multiplication rather than a loop over neuron objects.
  13. Calculate the pre-activation and ReLU output for each row of the batch introduced in Level 2.
  14. Explain why a batch of 100 observations does not give a one-neuron layer 100 sets of learned weights.
  15. State the input, weight, bias, and output shapes for a one-output nn.Linear layer applied to a batch of shape \((32,3)\).
  16. Explain why two units that start with identical weights and biases can remain redundant during training.

Summary

An artificial neuron calculates an affine transformation \(z=w^\top x+b\) and usually applies an activation \(a=g(z)\). Weights control input contributions, while the bias shifts the transformation. Activation functions prevent stacked layers from collapsing into one affine function. Modern neural networks execute many neuron-equivalent operations as batched matrix multiplications. Transformer feed-forward blocks use the same structure at a much larger scale: projection, activation, and projection back.

Looking ahead

One neuron produces one scalar activation. A neural-network layer produces a vector, and successive layers build increasingly complex representations. Chapter 6 develops layer dimensions, matrix operations, and composition. Chapter 7 then examines activation functions and non-linearity in greater depth.

Further reading

Early formal treatments of artificial neural units include McCulloch and Pitts (1943) and Rosenblatt (1958). The role of initialisation in deep networks is examined by Glorot and Bengio (2010). GELU is introduced by Hendrycks and Gimpel (2016), and the Transformer feed-forward architecture is described by Vaswani et al. (2017).

Glorot, Xavier, and Yoshua Bengio. 2010. “Understanding the Difficulty of Training Deep Feedforward Neural Networks.” Proceedings of the Thirteenth International Conference on Artificial Intelligence and Statistics, 249–56.
Hendrycks, Dan, and Kevin Gimpel. 2016. “Gaussian Error Linear Units (GELUs).” arXiv Preprint arXiv:1606.08415. https://arxiv.org/abs/1606.08415.
McCulloch, Warren S., and Walter Pitts. 1943. “A Logical Calculus of the Ideas Immanent in Nervous Activity.” The Bulletin of Mathematical Biophysics 5: 115–33. https://doi.org/10.1007/BF02478259.
Micikevicius, Paulius, Sharan Narang, Jonah Alben, et al. 2018. “Mixed Precision Training.” International Conference on Learning Representations. https://openreview.net/forum?id=r1gs9JgRZ.
Rosenblatt, Frank. 1958. “The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain.” Psychological Review 65 (6): 386–408. https://doi.org/10.1037/h0042519.
Vaswani, Ashish et al. 2017. “Attention Is All You Need.” Advances in Neural Information Processing Systems 30.