What is a tensor?
Numbers arranged along axes — and the one idea that lets a single formula work on all of them.
Every value in Hilbert may be a tensor. A price is a tensor, a column of prices is a tensor, a table of prices by store and month is a tensor, and a year of such tables is a tensor too. This page explains what that means, why mathematicians and physicists care, and how Hilbert uses it.
From numbers to tensors
The easiest way in is to count how many indices you need to point at one number.
| Rank | Name | You point at a number with | Example | Shape |
|---|---|---|---|---|
| 0 | scalar | nothing | a temperature, 21.5 |
[] |
| 1 | vector | one index i | daily sales for a week | [7] |
| 2 | matrix | two indices i, j | a grayscale image, a covariance matrix | [480, 640] |
| 3 | tensor | three indices i, j, k | a colour image (height × width × channel) | [480, 640, 3] |
| 4 | tensor | four indices | a batch of 32 colour images | [32, 480, 640, 3] |
The number of indices is the tensor’s rank (also called its order), and the list of extents is
its shape. A vector with shape [3] is written
a matrix with shape [2, 2] has entries \(A_{ij}\), and a rank-3 tensor has entries \(T_{ijk}\).
Nothing stops at three: rank is simply how many axes the numbers are arranged along.
In Hilbert the literals follow the same picture:
def price ≔ 21.5 -- rank 0
def week ≔ [3, 5, 4, 6, 8, 9, 7] -- rank 1, shape [7]
def grid ≔ [1, 2; 3, 4] -- rank 2, shape [2, 2]
def cube ≔ [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] -- rank 3, shape [2, 2, 2]
The physicist’s tensor
To a data scientist, a tensor is an array with axes — and for computation that is exactly right. The word comes from physics, though, where it means something stricter and more beautiful.
Push on a block of rubber and it deforms. The force per unit area on a surface inside the block depends on which way the surface faces: feed in the direction of the surface, and out comes the force on it. That rule — a machine that takes a direction and returns a vector — is the stress tensor \(\sigma_{ij}\). Written down in one coordinate system it is a 3×3 table of numbers. Rotate your axes and every number in the table changes, yet the physical fact it describes does not change at all.
That is the physicist’s definition: a tensor is a quantity whose components change, when you change coordinates, in exactly the way that keeps the thing itself the same. For a rank-2 tensor under a change of coordinates with matrix \(R\):
\[\sigma'_{ij} = \sum_{k} \sum_{l} R_{ik}\, R_{jl}\, \sigma_{kl}.\]The inertia tensor of a spinning body, the electromagnetic field tensor, the curvature of space-time in general relativity — all of them are tensors in this sense. Laws of nature written with tensors hold in every coordinate system at once, which is why Einstein needed them. Notice how many summation signs that one small transformation law already carries; that is where the story of this language’s central operation begins.
Indices say what to multiply and what to sum
Almost everything we do with vectors and matrices is a pattern of multiplying entries that share an index and summing over some of those indices:
| Operation | With sums | Indices kept |
|---|---|---|
| dot product | \(u \cdot v = \sum_i u_i v_i\) | none — a scalar |
| matrix times vector | \((Ax)_i = \sum_j A_{ij} x_j\) | i |
| matrix product | \((AB)_{ik} = \sum_j A_{ij} B_{jk}\) | i, k |
| outer product | \((u v^{\top})_{ij} = u_i v_j\) | i, j |
| trace | \(\operatorname{tr} A = \sum_i A_{ii}\) | none |
Reading these as index patterns rather than as five different operations is the insight Einstein
turned into notation, and it is how Hilbert computes all of them — with a single operation,
einsum. See Einstein’s summation for the history and einsum in practice
for the details.
Elementwise arithmetic and broadcasting
Arithmetic operators (+ − · /, ^) and the elementwise intrinsics (sin, exp, sqrt, comparisons…)
act entry by entry. [1, 2] · [3, 4] is [3, 8], not a dot product.
When the two shapes differ, Hilbert broadcasts them the way NumPy and ONNX do: shapes are aligned from the right, and an axis that is missing or has extent 1 is repeated to match.
def grid ≔ [1, 2; 3, 4]
fn scaledGrid(k) ↦ grid · k -- a scalar repeats everywhere
fn shifted(v) ↦ grid + v -- a vector repeats along the rows
Shapes.ScaledGrid(3); // [[3, 6], [9, 12]]
Shapes.Shifted(new[] { 10d, 20d }); // [[11, 22], [13, 24]]
Masks instead of branches
A comparison is a tensor of ones and zeros, so selection is multiplication. That is how the Prelude
expresses things that would be if statements elsewhere: “the k-th column”, “entries above the
threshold”, “the chosen action” are all masks.
fn clipped(x) ↦ min(max(x, 0), 1) -- [−2, 0.5, 3] gives [0, 0.5, 1]
fn above(x, t) ↦ x · (x > t) -- keep entries above t, zero the rest
Why one kind of value
Because a real number is just a rank-0 tensor, a Hilbert formula never has to say which kind of value
it expects. fn sum(x, y) ↦ x + y is compiled once per shape — plain C# for reals, an ONNX graph for
everything with axes — and the same formula serves a price, a price list and a price cube. See
Calling from C# for the overloads that are generated.
Shape matters, even for zero. x^0 is a tensor of ones shaped like x, and 0 has no shape at all.
That is why the Prelude builds a vector of ones as ones(x) ≔ x^0 and why the arithmetic laws never
rewrite x^0 to 1 — the shape would be lost. See Arithmetic.