nn.Module
Custom layers, registered parameters, and the list that silently does not train
nn.Sequential is fine until the moment your model does anything other than pass one thing straight through, which is almost immediately. A residual connection, two inputs, a branch that rejoins later: none of those fit in a list. nn.Module does.
The smallest possible module
from torch import nn
class MyLinear(nn.Module):
def __init__(self, n_in, n_out):
super().__init__() # never forget this line
self.weight = nn.Parameter(torch.randn(n_out, n_in) * 0.1)
self.bias = nn.Parameter(torch.zeros(n_out))
def forward(self, x):
return x @ self.weight.T + self.bias
layer = MyLinear(4, 3)
x = torch.randn(2, 4)
print('output shape', tuple(layer(x).shape))
print('\nparameters it registered:')
for name, prm in layer.named_parameters():
print(' %-8s %s' % (name, tuple(prm.shape)))
parameters it registered:
weight (3, 4)
bias (3,)
What nn.Parameter is for
A plain tensor assigned to self is invisible. Wrap it in nn.Parameter and the module registers it, which means it appears in model.parameters(), gets passed to the optimiser, moves with .to(device) and is saved in state_dict(). Forget the wrapper and the tensor simply never trains, silently, while everything else does.
from torch import nn
class Forgetful(nn.Module):
def __init__(self):
super().__init__()
self.good = nn.Parameter(torch.zeros(3))
self.bad = torch.zeros(3) # not a Parameter
m = Forgetful()
print('registered parameters:', [n for n, _ in m.named_parameters()])
print('the optimiser would update %d tensor(s)'
% len(list(m.parameters())))
print('\nself.bad exists and is used in forward, but never learns.')
the optimiser would update 1 tensor(s)
self.bad exists and is used in forward, but never learns.
Modules contain modules
from torch import nn
class Block(nn.Module):
def __init__(self, size):
super().__init__()
self.fc = nn.Linear(size, size)
self.act = nn.ReLU()
def forward(self, x):
return self.act(self.fc(x))
class Net(nn.Module):
def __init__(self):
super().__init__()
self.stem = nn.Linear(784, 64)
self.blocks = nn.ModuleList([Block(64) for _ in range(3)])
self.head = nn.Linear(64, 10)
def forward(self, x):
x = torch.relu(self.stem(x.flatten(1)))
for block in self.blocks:
x = block(x)
return self.head(x)
net = Net()
print('parameters %d' % sum(p.numel() for p in net.parameters()))
print('output', tuple(net(torch.randn(5, 1, 28, 28)).shape))
print('\nnamed modules:')
for name, mod in net.named_children():
print(' %-8s %s' % (name, type(mod).__name__))
output (5, 10)
named modules:
stem Linear
blocks ModuleList
head Linear
A plain Python list hides its modules
self.blocks = [Block(64) for _ in range(3)] looks identical and is broken in the same way as a plain tensor: the modules in it are not registered, so their parameters do not reach the optimiser and do not move to the GPU. Use nn.ModuleList when you need a list and nn.ModuleDict when you need a dictionary. The symptom is a model that trains, slowly, using only the layers you happened to assign directly.
A residual connection, which Sequential cannot express
from torch import nn
class Residual(nn.Module):
def __init__(self, size):
super().__init__()
self.fc1 = nn.Linear(size, size)
self.fc2 = nn.Linear(size, size)
def forward(self, x):
h = torch.relu(self.fc1(x))
h = self.fc2(h)
return torch.relu(x + h) # the input rejoins the output
block = Residual(16)
x = torch.randn(4, 16)
print('in ', tuple(x.shape), ' out', tuple(block(x).shape))
print('\nthat one addition is the idea week 6 is built on.')
that one addition is the idea week 6 is built on.
Day 1 takeaway
Subclassnn.Module, call super().__init__() first, wrap learnable tensors in nn.Parameter and lists of layers in nn.ModuleList. Anything that is not registered does not train, and nothing warns you.