Lesson 3 of 7

Broadcasting, rule by rule

Two arrays of different shapes can be combined without a loop and without a copy. One rule decides whether they can, and one shape comes out. Both are worth being able to do in your head.

The last lesson ended on a length-one axis: (3, 1) against (1, 3) does not fail, it gives you a (3, 3) grid of every pair. This lesson is why, and what to do about it.

Six ideas and one rule, applied by hand until it is automatic: line the shapes up from the right, pad the shorter one with ones, and every aligned pair must match or contain a one. Break it and about half the time you get a loud error. The other half is the bug this lesson ends on.

Every board is the same rule applied by hand: right-align the shapes, pad the shorter one with ones, and check that each pair is equal or contains a one. Nothing runs — the answer is the rule, which is exactly why it is worth being able to do in your head. A board that raises is as valid an answer as a shape, and the shape that comes out of a mistake is often the more interesting one.

1 of 6

The loop you do not write

Adding a bias to a batch, scaling a feature column, normalizing a row -- all of it is one array meeting another of a different shape.

A batch of 32 examples with 10 features each is an array of shape (32, 10). One bias per feature is an array of shape (10,). You want to add the bias to every row.

There are two obvious ways to do that and both are wrong:

for i in range(32):           # a Python loop, once per row
    x[i] = x[i] + b

x + np.tile(b, (32, 1))      # a real (32, 10) copy of the bias

And the way NumPy is actually written:

x + b                        # (32, 10) + (10,) -> (32, 10)

NumPy reads the two shapes, works out that the (10,) can stand in for every one of the 32 rows, and runs a single compiled loop over the result. There is no Python loop, and there is no tiled copy of the bias anywhere in memory. That is broadcasting.

The same rule is doing the work in almost every line of array code you will write:

x - 0.5                              a scalar against everything
x * scale                            (32, 10) * (10,)   one scale per feature
x - x.mean(axis=0)                   (32, 10) - (10,)   center each column
x / x.sum(axis=1, keepdims=True)     (32, 10) / (32, 1)  normalize each row

None of those is a convenience over a loop you could have written. Vectorized code is written in shapes, and broadcasting is the rule that says which shapes go together.

The memory is worth counting. Tiling a (10,) bias up to (32, 10) allocates 320 numbers -- annoying but survivable. Do it for a batch of 100,000 and you have allocated a second copy of the entire batch to hold ten distinct values. Broadcasting allocates none of it.

Challenge

Without the Loop

1 of 4

Say what comes out, and notice that no copy was made to get it.

Four lines you will write constantly. Each one combines two different shapes, and each one runs as a single compiled loop with nothing tiled in between. Give the shape of the result.