Linear algebra · foundations Section 1.1 · item 1 of 29

Scalars, vectors,
matrices, tensors

There is only one object here. A scalar is a number, a vector is a line of numbers, a matrix is a grid, a tensor is any of those with as many axes as you like. The names are just labels for how many directions you can travel in.

Learn to read a shape fluently and a large fraction of machine learning's day-to-day friction disappears. Nearly every error a beginner hits is a shape error wearing a different hat.

The whole idea

rank = how many indices it takes to reach a single number

Zero indices: scalar. One: vector. Two: matrix. Three or more: we run out of common words and say tensor.

Build one. Click into it.

Pick a rank, size the axes, then click any cell. The panel tells you the index expression that reaches it and where that element physically sits in memory.

Object
Shape
Rank · ndim
Size · total elements
Click a cell to select an element.

Memory position assumes row-major (C) order, the default in NumPy and PyTorch: the last axis varies fastest.

01The four objects

One family, four names

These are not four different things. They are the same construct — an array of numbers — distinguished only by how many axes it has. Everything you learn about one carries to the others.

Scalar · rank 0

A single number. A learning rate, a loss value, a count. Notation: italic lowercase, α or n.

shape ()

Vector · rank 1

An ordered list. One data point's features, a word embedding, a gradient. Notation: bold lowercase, v.

shape (4,)

Matrix · rank 2

A grid. A dataset of rows and features, a weight matrix, a grayscale image. Notation: bold uppercase, A.

shape (3, 3)

Tensor · rank 3+

Any number of axes. A colour image, a batch of anything, an attention map. Notation: bold sans-serif or calligraphic.

shape (3, 3, 3)

"Tensor" is used loosely here — and that's fine, but know it. In physics and differential geometry a tensor is a multilinear map with specific transformation rules under a change of basis. In machine learning, "tensor" almost always just means n-dimensional array. The libraries chose the word; the mathematical object is a stricter thing. You will occasionally meet someone who cares about the distinction, and they are not wrong.

Notation conventions in papers

a, n, λ
Scalars — italic lowercase
x, w
Vectors — bold lowercase (sometimes with an arrow)
A, W, X
Matrices — bold uppercase
𝓧, 𝗧
Higher-order tensors — calligraphic or sans-serif bold
xi
The ith element of vector x — italic, since one element is a scalar
Aij
Row i, column j of A — row index first, always

Conventions vary by field and author. Check the notation section of any paper before assuming.

The column-vector default

In mathematical writing, an unqualified vector is a column — shape (n, 1). That's why you see Wx rather than xW, and why transposes appear so often: xy is the dot product because it's (1,n) times (n,1).

Code disagrees. A NumPy array of shape (n,) is neither a row nor a column — it has one axis and no orientation at all. This mismatch between the maths on the page and the array in memory is the source of an enormous number of bugs. Section 03 deals with it directly.
02Rank, shape, size

Three numbers describe any array

Everything you need to know about an array's structure fits into three quantities. Get comfortable saying all three out loud for any object you meet.

Rank · ndim · order

How many axes it has. How many indices you must supply to land on a single number. A matrix has rank 2 because you need a row and a column.

len(shape)

Shape

A tuple giving the length of each axis, outermost first. (32, 128) means 32 along axis 0 and 128 along axis 1. This is the number you will read a thousand times a day.

x.shape

Size

Total element count — the product of the shape. Multiply by the bytes per element and you have the memory footprint, which is often the number that actually matters.

x.size · prod(shape)

"Dimension" means three different things, and people switch between them mid-sentence. (1) The number of axes — "a 3-dimensional tensor". (2) The length of one axis — "the batch dimension is 32". (3) The length of a vector in the linear-algebra sense — "a 768-dimensional embedding", which is a rank-1 array. When someone says "dimension", work out which of the three they mean before responding. Prefer rank for the count of axes and axis length for the size of one; the ambiguity vanishes.
ObjectRankShapeSizeIndices to reach one number
Loss value0()1loss — none needed
Embedding1(768,)768v[41]
Tabular batch2(32, 10)320X[5, 3]
Colour image3(3, 224, 224)150,528img[0, 100, 57]
Batch of images4(32, 3, 224, 224)4,816,896b[7, 0, 100, 57]
Attention scores4(32, 12, 512, 512)100,663,296a[7, 3, 40, 88]

That last row is worth a pause. At 4 bytes per float that single tensor is about 400 MB — for one layer, one forward pass. Reading shapes is not an academic exercise; it is how you predict whether something will fit on the card.

03Reading a shape

How to read (32, 3, 224, 224)

Read left to right as outermost container to innermost. The leftmost axis is the one you'd loop over first; the rightmost is the one whose elements sit next to each other in memory.

Common layout conventions

(N, F)
Tabular — N rows, F features. The default for scikit-learn.
(N, C, H, W)
Images, PyTorch order — batch, channels, height, width. Called NCHW.
(N, H, W, C)
Images, TensorFlow order — NHWC. Same data, different axis order. A frequent source of silent corruption when moving between frameworks.
(N, L, D)
Sequences — batch, sequence length, model width. The standard transformer layout.
(L, N, D)
Sequence-first. PyTorch's RNN modules default to this unless you pass batch_first=True.
(N, h, L, L)
Attention weights — batch, heads, query positions, key positions.
The batch axis is a convention, not a law. Nothing marks axis 0 as "the batch" except everyone's agreement. If you feed a single image of shape (3, 224, 224) to a model expecting a batch, the layer will interpret 3 as your batch size and quietly do something wrong. unsqueeze(0) exists for exactly this.
The (n,) versus (n,1) versus (1,n) trap. These are three different objects. (n,) is rank 1 with no orientation; (n,1) is a column matrix; (1,n) is a row matrix. Broadcasting treats them very differently, and mixing them is how you accidentally produce an (n,n) array from two length-n vectors and get no error at all.
# the classic silent disaster
y_true = np.array([1, 2, 3])          # shape (3,)
y_pred = np.array([[1], [2], [4]])    # shape (3, 1)

err = y_true - y_pred                # no error raised!
print(err.shape)                    # (3, 3)  ← broadcasting, not subtraction
print((err ** 2).mean())            # a completely meaningless number

# fix: make the shapes agree before you compute anything
y_pred = y_pred.ravel()              # (3,)  — or y_true.reshape(-1, 1)
04Indexing

Reaching in — and what it does to the rank

Every index you supply removes one axis. Every slice you supply keeps one. That single rule explains most of what happens to shapes when you index.

ExpressionMeaningResult shape from (4, 5, 6)
x[2]One integer — drops axis 0(5, 6)
x[2, 1]Two integers — drops two axes(6,)
x[2, 1, 0]All three — lands on a scalar()
x[2:3]A slice — keeps the axis, length 1(1, 5, 6)
x[:, 1]Keep axis 0, index axis 1(4, 6)
x[:, :, 0]Index the last axis(4, 5)
x[..., 0]Ellipsis — "all remaining axes", same as above(4, 5)
x[-1]Negative index — counts from the end(5, 6)
x[None]Insert a new axis of length 1 at the front(1, 4, 5, 6)
x[:, None]Insert a new axis in position 1(4, 1, 5, 6)
x[[0, 2]]Fancy indexing — a list selects several(2, 5, 6)
x[mask]Boolean mask — result depends on how many are True(k, 5, 6)
The rank rule, stated once: integer indices remove an axis, slices preserve it, and None adds one. If you can apply those three facts you can predict any indexing result without running it.
x[2] and x[2:3] are not the same. They contain identical data and have different ranks. This bites when a downstream function expects a specific rank — which is most of them.
05Axes & reductions

axis=0 means "collapse axis 0", not "along the rows"

This is the single most misremembered thing in array programming, and it has a clean mental model: the axis you name is the one that disappears. Everything else survives.

Reducing a (3, 4) matrix · the named axis is the one removed

Worked through

Starting from shape (3, 4):

sum(axis=0)
Axis 0 collapses → (4,). One number per column. This is the column-wise total.
sum(axis=1)
Axis 1 collapses → (3,). One number per row.
sum(axis=-1)
The last axis, whatever it is → (3,). Safer than a hard-coded number when rank varies.
sum()
All axes collapse → (), a scalar.
sum(axis=(0,1))
Several at once → () here.
keepdims=True
Collapse to length 1 instead of removing → (1, 4). Keeps the rank so the result still broadcasts against the original.
Why keepdims matters. Normalizing rows is the standard case: x / x.sum(axis=1, keepdims=True) works because (3,1) broadcasts cleanly against (3,4). Without keepdims you get (3,), which right-aligns against the wrong axis and either errors or — worse — silently normalizes the columns.
Softmax is where this goes wrong most often. Over a batch of logits shaped (N, C) you want axis=-1, normalizing across classes for each example. Use axis=0 and you normalize each class across the batch, which produces plausible-looking numbers that mean nothing. No error, no warning.
06Reshaping

Changing the shape without changing the numbers

Two operations get confused constantly. Reshape keeps the elements in the same memory order and reinterprets where the axis boundaries fall. Transpose genuinely reorders which element is where. They are not interchangeable, and swapping them produces scrambled data rather than an error.

Same input, same output shape, different contents
OperationDoesExample on (2, 3, 4)
reshape(a, b)Reinterprets element boundaries; total size must be preserved→ (6, 4)
reshape(-1, 4)-1 means "infer this one from the rest"→ (6, 4)
flatten() / ravel()Collapse everything to rank 1→ (24,)
transpose() / .TReverse the axis order→ (4, 3, 2)
permute(2, 0, 1)Reorder axes explicitly — the readable choice above rank 2→ (4, 2, 3)
swapaxes(0, 1)Exchange exactly two axes→ (3, 2, 4)
squeeze()Drop every axis of length 1(1,3,1) → (3,)
unsqueeze(0) / [None]Insert an axis of length 1→ (1, 2, 3, 4)
expand / broadcast_toRepeat along a length-1 axis without copying memory(1,4) → (3,4)
stackJoin arrays along a new axis3×(2,4) → (3,2,4)
concatenate / catJoin along an existing axis3×(2,4) → (6,4)
The tell that you've confused them: your loss is finite but the model refuses to learn, or your images come out looking like static. Reshape never errors when the sizes happen to match — (N,H,W,C).reshape(N,C,H,W) is perfectly legal and completely destroys the image. You wanted permute(0, 3, 1, 2).
07Shape calculator

Broadcasting and matmul, checked live

Two rules cover almost every shape question you'll have. Type any pair of shapes and watch them resolve — or fail, with the axis that caused it named.

+

The broadcasting rule

  1. Line the two shapes up from the right.
  2. Pad the shorter one with 1s on the left.
  3. Each pair of axes must be equal, or one of them must be 1.
  4. Any axis of length 1 is stretched to match the other.
  5. If any pair fails both tests, the operation errors.

Stretching is virtual — no memory is copied. That's why broadcasting is fast, and also why an accidental broadcast can quietly allocate an enormous result.

The matmul rule

(n, k) @ (k, m) → (n, m)

The inner dimensions must match and they vanish. The outer two survive, in order. Everything about matrix multiplication shapes follows from this.

Above rank 2 it becomes batched: the last two axes multiply by the rule above, and all leading axes broadcast against each other. So (8, 4, 3, 5) @ (8, 4, 5, 2) gives (8, 4, 3, 2) — 32 independent small matrix products.

@ is not *. The asterisk is elementwise multiplication with broadcasting; the at-sign is matrix multiplication. Both are legal on the same pair of arrays and they compute entirely different things.
08Memory

It's all one flat strip

Underneath every array of every rank is a single contiguous run of numbers. The shape is metadata describing how to walk it. This is why some operations are free and others cost a full copy.

Row-major (C) order — the last axis varies fastest

Strides

A stride is how far to step in memory to advance one position along an axis. A (2,3) float32 array has strides (12, 4) bytes: move 4 bytes for the next column, 12 for the next row.

Transposing doesn't move a single number — it swaps the strides. The data is untouched; only the walking instructions change. That's why .T is instant even on huge arrays.

Views versus copies

A view shares memory with the original; writing to it changes both. Slicing, transposing and reshaping usually produce views. A copy is independent.

Two consequences. Modifying a slice in place mutates the parent, which surprises people constantly. And .view() in PyTorch fails on a non-contiguous tensor — after a transpose you often need .contiguous() first, or just use .reshape(), which copies when it has to.

Row-major vs column-major

NumPy, PyTorch, C and C++ are row-major: the last index moves fastest. Fortran, MATLAB, R and Julia are column-major: the first index moves fastest. Same logical array, different physical order. It matters when interfacing between them, and it matters for performance — iterating along the fast axis is dramatically quicker because it uses cache well.

Dtype, and why it's half the memory question

Shape tells you element count; dtype tells you bytes per element. float64 is 8, float32 is 4 and is the deep-learning default, float16 and bfloat16 are 2, int8 is 1. Halving precision halves memory and usually increases speed — which is the entire basis of mixed-precision training and quantized inference.

09In practice

Shapes you'll meet constantly

WhereShapeReading
Tabular X(n_samples, n_features)The scikit-learn contract. Everything expects this
Tabular y(n_samples,)Rank 1. Passing (n,1) triggers warnings in many estimators
Linear layer weight(out_features, in_features)Note the order — output first. Catches everyone once
Conv2d weight(out_ch, in_ch, kH, kW)A stack of 3-D filters, one per output channel
Image batch (torch)(N, C, H, W)Channels before spatial dims
Image batch (TF)(N, H, W, C)Channels last
Token ids(N, L)Batch of integer sequences, before embedding
Embedded tokens(N, L, D)The embedding lookup added an axis of width D
Q, K, V per head(N, h, L, d_head)D was split into h heads of width d_head
Attention weights(N, h, L, L)Square in the last two axes — the quadratic cost, visible
Classifier logits(N, n_classes)Softmax over axis=-1
Loss()Rank 0. backward() requires a scalar
A habit worth building: annotate every tensor in your code with its shape in a trailing comment, and update the comments when they go stale. It costs seconds and turns shape debugging from archaeology into reading.
10Debugging

Reading the error, and preventing the next one

What the messages mean

ValueError: operands could not be
broadcast together with shapes
(3,4) (5,4)

Right-align them: 4 vs 4 fine, 3 vs 5 fails and neither is 1. Axis 0 is the culprit.

RuntimeError: mat1 and mat2 shapes
cannot be multiplied (32x128 and 64x10)

Inner dims 128 and 64 disagree. Your layer expects 64 inputs and got 128 — usually a flatten computed for a different input size.

Techniques that actually help

  • Print shapes at every step before you theorize. Two minutes of printing beats twenty of reasoning.
  • Assert them. assert x.shape == (B, L, D), x.shape fails at the cause rather than three layers downstream.
  • Run one example through by hand with batch size 1 and tiny dimensions. Errors are far easier to read at (2,3,4) than (64,512,768).
  • Use einsum for anything non-obvious. einsum('bld,dk->blk', x, W) states the contraction explicitly and is self-documenting.
  • Prefer -1 and axis=-1 over hard-coded positions, so code survives a rank change.
  • Check permute versus reshape first whenever data looks corrupted rather than absent.
# shape assertions as living documentation
def attention(q, k, v):
    B, H, L, D = q.shape
    assert k.shape == (B, H, L, D), k.shape
    scores = q @ k.transpose(-2, -1)        # (B,H,L,D) @ (B,H,D,L) -> (B,H,L,L)
    assert scores.shape == (B, H, L, L), scores.shape
    w = scores.softmax(dim=-1)                # normalize over KEYS, the last axis
    out = w @ v                                # (B,H,L,L) @ (B,H,L,D) -> (B,H,L,D)
    return out.transpose(1, 2).reshape(B, L, H * D)   # merge heads back
11Practice

Drills that build the fluency

#Do thisYou'll know it when
1For ten random shapes, say rank, size, and memory footprint in float32 out loud before checkingYou stop needing to check
2Predict the result of twenty indexing expressions on a (4,5,6) array, then verifyInteger-drops-an-axis is automatic
3Take a (3,4) matrix and reduce it every way — each axis, both, with and without keepdimsYou never guess at axis= again
4Write ten shape pairs and decide by hand whether they broadcast, then check with the calculator aboveRight-alignment is reflex
5Reshape and transpose the same array to the same target shape; diff the resultsYou can explain exactly why they differ
6Print .strides before and after a transposeYou understand why transposing is free
7Trace one image batch through a small CNN, writing the shape after every layerYou can compute the flatten size yourself
8Implement single-head attention from shapes alone, asserting at every stepQ, K, V shapes stop being memorized and start being derived

The one-paragraph summary

Scalars, vectors, matrices and tensors are one object at different ranks, where rank is the number of indices needed to reach a single number. Shape is the tuple of axis lengths, read outermost to innermost; size is their product; multiply by the dtype's byte width for the memory cost. Integer indices remove axes, slices keep them, None adds them. Reductions remove the axis you name unless you pass keepdims. Reshape reinterprets the flat memory strip while transpose reorders it, and confusing the two corrupts data without raising an error. Broadcasting aligns shapes from the right and stretches any axis of length 1; matrix multiplication requires the inner dimensions to match and eliminates them. Underneath all of it is one contiguous run of numbers plus a set of strides describing how to walk it — which is why transposing is free, why views alias their parent, and why the fast axis is the last one.