Every model in this roadmap is a long chain of operations on tables of numbers. Before backpropagation, attention or sampling, you need to be comfortable with four things, and this page explains each one with small examples:
- what a NumPy array is, and what its shape and axes mean;
- how broadcasting combines arrays of different shapes;
- why
expof a large number breaks, and the two tricks that fix it; - two indexing tools that come back in every later section.
Arrays, shapes and axes
A NumPy array is a table of numbers, all of the same type. A list of numbers is a 1-D array, a table with rows and columns is a 2-D array, and you can keep going: a stack of tables is 3-D, and so on.
The shape says how many numbers there are along each direction. Each direction is called an axis, and axes are numbered from 0.
Example.
x = np.array([[1, 2, 3], [4, 5, 6]])is a table with 2 rows and 3 columns, sox.shapeis(2, 3).
- Axis 0 goes down the rows (length 2).
x.sum(axis=0)adds up each column, moving down:[1+4, 2+5, 3+6] = [5, 7, 9].- Axis 1 goes across the columns (length 3).
x.sum(axis=1)adds up each row, moving across:[1+2+3, 4+5+6] = [6, 15].Summing along an axis makes that axis disappear: shape
(2, 3)becomes(3,)or(2,).
Behind the scenes, an array is one block of memory plus two pieces of bookkeeping: its shape, and its strides, how many bytes to jump in memory to reach the next number along each axis. For the x above, stored as 8-byte numbers, the strides are (24, 8): the next column is 8 bytes away (one number), the next row 24 bytes away (three numbers). This is why operations such as transposing are instant: NumPy only changes the bookkeeping, not the numbers.
Think in arrays, not loops
An expression like x @ W + b (a matrix product, then an addition) runs as compiled loops inside NumPy, so it is often orders of magnitude (factors of 10, 100 or more) faster than the same arithmetic written as a Python for loop.
The rule for every exercise here: no Python loops over elements. If you catch yourself writing one, look for the array expression that does the same job.
Example. To add 1 to every number of
x, do not loop over rows and columns: writex + 1. The result is[[2, 3, 4], [5, 6, 7]].
Always know the shape of every array, and write it next to the code, for example # (N, D) for rows of numbers. Most bugs in ML (machine learning) code are shape bugs, and NumPy often hides them instead of raising an error, because of broadcasting.
Broadcasting
Broadcasting is what NumPy does when two arrays of different shapes meet in an operation that works number by number (+, -, *, /). It follows three steps:
- Write the two shapes one above the other, aligned to the right. If one shape is shorter, pad it on the left with 1s.
- Compare them axis by axis. Two sizes fit together when they are equal, or when one of them is 1. Otherwise NumPy raises an error.
- An axis of size 1 is repeated to match the other size, without actually copying any memory.
Example 1: a row added to every row.
xhas shape(2, 3)andb = [10, 20, 30]has shape(3,).
- Aligned to the right:
(2, 3)and(3,), sobis treated as shape(1, 3).- Axis 1: 3 and 3 are equal. Axis 0: 2 and 1, fine because one is 1.
bis repeated down the 2 rows.x + bis[[11, 22, 33], [14, 25, 36]].
Example 2: every pair. A column
c = [[1], [2], [3]], shape(3, 1), plus a rowr = [[10, 20]], shape(1, 2). Both axes have a 1, so the column is repeated across 2 columns and the row down 3 rows.c + ris the full table of sums,[[11, 21], [12, 22], [13, 23]]: entry isc[i] + r[j].
Example 2 is the trick behind every "all pairs" computation. To turn a row of numbers into a column, insert an axis of size 1 with x[:, None] (None is the same as np.newaxis).
Example 3: the classic bug. You want to divide every row of
xby its own sum, so that each row adds up to 1. The row sumsx.sum(axis=1)are[6, 15], with shape(2,).
x / x.sum(axis=1)aligns(2, 3)with(2,)to the right: 3 against 2, neither is 1, so NumPy raisesoperands could not be broadcast together. Worse, with a square table it would not raise: it would divide column by the sum of row , and return wrong numbers without any warning.x / x.sum(axis=1, keepdims=True)works. Withkeepdims=Truethe summed axis is kept with size 1, so the sums have shape(2, 1): a column, repeated across. The result is[[1/6, 2/6, 3/6], [4/15, 5/15, 6/15]], about[[0.167, 0.333, 0.5], [0.267, 0.333, 0.4]].
This is why reductions such as sum and max take keepdims=True: their result keeps a size-1 axis, so it lines up with the array it came from.
Floating point and numerical stability
Computers store real numbers as floating-point numbers. NumPy's default, float64, keeps about 16 significant decimal digits, and its largest value is about . Anything bigger becomes inf (infinity), and anything too close to 0 becomes exactly 0:
- still fits, but is
inf; - is the smallest number left, and is exactly
0.
This matters because models produce logits: raw scores, any real numbers, one per possible answer. The next step is almost always , and logits of a few hundred or a few thousand do happen. When they overflow, the training loss becomes nan ("not a number") and training stops making sense. Two tricks fix almost everything.
Trick 1: shift before softmax
Softmax turns a list of scores into probabilities: positive numbers that add up to 1. It takes to the power of each score, then divides by the total:
where:
- is the list of scores and is one of them;
- is always positive, and bigger scores give bigger values;
- is the total of all of them, so dividing by it makes the results add up to 1.
Example. For : , , , total . Dividing each by the total gives , which adds up to 1.
With the same steps compute , which is inf, and inf / inf is nan. The fix: subtracting the same number from every score does not change the result,
because , and the same factor appears above and below the fraction, so it cancels. Choose = the largest score: then the largest exponent is and nothing can overflow.
Example. For , subtract : the scores become . Then and , total , and the softmax is , the correct answer.
Trick 2: log-sum-exp
Training works with the logarithm of probabilities, and needs the quantity "log of the sum of the exponentials", , called log-sum-exp. It overflows for the same reason, and it has its own fix: pull the largest score out of the sum.
where:
- are the scores;
- is the largest of them.
Why it works: after subtracting , the largest exponent is , so nothing overflows. And the sum contains that 1, so it is at least 1, and its log is never . This is why frameworks compute log_softmax(x) = x - logsumexp(x) directly, instead of log(softmax(x)).
Example. For : , the shifted scores are , the sum is , so the result is . The naive formula would compute .
Two indexing tools
Picking one entry per row
Integer-array indexing gives NumPy two lists, one of row numbers and one of column numbers, and it pairs them up position by position. P[np.arange(N), targets] reads, from each row i, the column targets[i]. This is how you read the probability of the correct token out of a table of probabilities.
Example.
P = [[0.2, 0.7, 0.1], [0.5, 0.1, 0.4]]andtargets = [1, 2].np.arange(2)is[0, 1], soP[[0, 1], [1, 2]]readsP[0, 1]andP[1, 2]: the result is[0.7, 0.4].
np.einsum: products and sums by letters
np.einsum lets you describe a computation by giving each axis a letter. Letters in the inputs that are missing from the output are summed over; the others are kept.
Example. The matrix product
A @ Bisnp.einsum("ij,jk->ik", A, B): for each rowiofAand columnkofB, multiply along the shared letterjand add up. WithA = [[1, 2], [3, 4]]andB = [[5, 6], [7, 8]], entry is , and the full result is[[19, 22], [43, 50]].
The same notation scales to the 4-D arrays of attention: np.einsum("bhid,bhjd->bhij", q, k) computes all the attention scores for every batch b and head h in one line. The letter d is missing from the output, so it is summed over.
References
- C. R. Harris et al., Array programming with NumPy, Nature 585, 357–362, 2020. How arrays, strides and broadcasting are designed.
- NumPy documentation, Broadcasting and
numpy.einsum. - D. Goldberg, What every computer scientist should know about floating-point arithmetic, ACM Computing Surveys 23(1), 5–48, 1991.
- P. Blanchard, D. J. Higham, N. J. Higham, Accurately computing the log-sum-exp and softmax functions, IMA Journal of Numerical Analysis 41(4), 2311–2330, 2021.
- I. Goodfellow, Y. Bengio, A. Courville, Deep Learning, MIT Press, 2016. Chapter 4: Numerical Computation.