Tensors and Shapes
The array that remembers, and the broadcast that hides your bug
Deep learning has one idea in it. You write down a function with a very large number of adjustable numbers in it, you measure how wrong it is, and you work out which way to nudge every one of those numbers to make it less wrong. Everything else on this course is detail about how to do that quickly and without the whole thing falling over.
This week is about the machinery underneath. If you have used Keras or PyTorch before and found it mostly worked until it did not, this is the week that fixes that, because almost every confusing failure later on is a shape problem or a gradient problem, and both live here.
A tensor is an array that remembers what happened to it
scalar = torch.tensor(3.0)
vector = torch.tensor([1.0, 2.0, 3.0])
matrix = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
batch = torch.zeros(8, 3, 32, 32) # 8 images, 3 channels, 32 by 32
for name, t in [('scalar', scalar), ('vector', vector),
('matrix', matrix), ('batch', batch)]:
print('%-8s shape %-22s dim %d dtype %s'
% (name, str(tuple(t.shape)), t.dim(), t.dtype))
vector shape (3,) dim 1 dtype torch.float32
matrix shape (2, 2) dim 2 dtype torch.float32
batch shape (8, 3, 32, 32) dim 4 dtype torch.float32
Read the shape, always
The single most common error in this subject is a shape mismatch, and the single most useful debugging habit is printing tensor.shape before and after anything you are unsure about. By convention the first dimension is the batch, so a tensor of shape (8, 3, 32, 32) is eight images, each with three colour channels, each 32 pixels square. Nothing enforces that convention. It is a habit that every library and every paper follows.
The operations you will use constantly
a = torch.arange(6.0).reshape(2, 3)
b = torch.ones(3, 4)
print('a')
print(a)
print('a @ b shape', tuple((a @ b).shape))
print('a.T shape ', tuple(a.T.shape))
print('sum ', a.sum().item())
print('sum along rows ', a.sum(dim=0))
print('sum along columns', a.sum(dim=1))
print('mean of everything %.3f' % a.mean().item())
tensor([[0., 1., 2.],
[3., 4., 5.]])
a @ b shape (2, 4)
a.T shape (3, 2)
sum 15.0
sum along rows tensor([3., 5., 7.])
sum along columns tensor([ 3., 12.])
mean of everything 2.500
dim=0 means collapse the rows and keep the columns, which is the direction you want when you are summarising a batch. It is worth saying that out loud a few times, because getting it backwards produces code that runs, returns a number of the right type, and is silently wrong.
Broadcasting
batch = torch.ones(4, 3) # 4 rows, 3 features
bias = torch.tensor([10.0, 20.0, 30.0])
print('batch + bias')
print(batch + bias)
column = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
print('\nbatch * column')
print(batch * column)
try:
torch.ones(4, 3) + torch.ones(4)
except RuntimeError as e:
print('\nand when it cannot:')
print(' ', str(e)[:96])
tensor([[11., 21., 31.],
[11., 21., 31.],
[11., 21., 31.],
[11., 21., 31.]])
batch * column
tensor([[1., 1., 1.],
[2., 2., 2.],
[3., 3., 3.],
[4., 4., 4.]])
and when it cannot:
The size of tensor a (3) must match the size of tensor b (4) at non-singleton dimension 1
The broadcast that does not fail is the one to fear
Adding a tensor of shape (4,) to one of shape (4, 1) does not raise. It broadcasts to (4, 4), and you now have sixteen numbers where you wanted four. This is the classic silent bug in a loss function: your targets come back as (batch,), your predictions as (batch, 1), and the loss you compute is the mean of every pairing rather than the mean of the matched pairs. It trains. It just trains on nonsense.
pred = torch.tensor([[0.9], [0.2], [0.7], [0.1]]) # (4, 1)
target = torch.tensor([1.0, 0.0, 1.0, 0.0]) # (4,)
wrong = ((pred - target) ** 2).mean()
right = ((pred.squeeze(1) - target) ** 2).mean()
print('difference shape when broadcast:', tuple((pred - target).shape))
print('loss computed carelessly %.4f' % wrong.item())
print('loss computed correctly %.4f' % right.item())
print('\nneither raised an error.')
loss computed carelessly 0.3625
loss computed correctly 0.0375
neither raised an error.
Day 1 takeaway
A tensor is a NumPy array that can record its own history. Print shapes constantly, know thatdim=0 collapses rows, and treat any mismatch between (n,) and (n, 1) as a bug waiting to happen, because broadcasting will hide it rather than report it.