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 biasAnd 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 rowNone 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.