3.3.3 · D4Deep Learning Frameworks

Exercises — Building models with nn.Module

2,137 words10 min readBack to topic

This is your self-testing arena for Building models with nn.Module. Every problem has a fully worked solution hidden inside a collapsible callout — try first, then reveal. Problems climb from recognising the API to synthesising whole architectures and reasoning about gradients.

Before you start, one tiny vocabulary check so no symbol sneaks in undefined:

Figure — Building models with nn.Module

Level 1 — Recognition

Exercise 1.1

Which of these is registered as a trainable parameter by PyTorch, and which is silently ignored?

class A(nn.Module):
    def __init__(self):
        super().__init__()
        self.w = nn.Parameter(torch.randn(5))     # (a)
        self.v = torch.randn(5)                    # (b)
        self.layers = [nn.Linear(3, 3)]            # (c)
        self.layers2 = nn.ModuleList([nn.Linear(3, 3)])  # (d)
Recall Solution
  • (a) registered — it is an nn.Parameter, so __setattr__ records it. ✅
  • (b) NOT registered — a plain torch.randn(5) tensor. It rides along on .to(device) only if you register it as a buffer; as written it is invisible to .parameters(). ❌
  • (c) NOT registered — a plain Python list. PyTorch's __setattr__ never looks inside ordinary lists, so the nn.Linear inside is a ghost: it exists, but .parameters() never finds it and the optimizer never trains it. ❌
  • (d) registerednn.ModuleList is the special container that does register everything inside. ✅

Exercise 1.2

What single method call gives you every learnable tensor of a model, and what does it return?

Recall Solution

model.parameters() returns an iterator over all nn.Parameter tensors, collected by recursively walking the submodule tree. To count numbers: sum(p.numel() for p in model.parameters()). To also see names: model.named_parameters().


Level 2 — Application

Exercise 2.1

Count the parameters of nn.Linear(100, 50).

Recall Solution

Weights: . Bias: . Total .

Exercise 2.2

Build a classifier nn.Linear(784, 256) → ReLU → nn.Linear(256, 10). How many parameters total?

Recall Solution
  • Layer 1: .
  • ReLU is stateless — zero parameters.
  • Layer 2: .
  • Total .

Exercise 2.3

A DeepNetwork(input_dim=64, hidden_dim=32, num_layers=3, output_dim=8) follows the parent's pattern: input_layer = Linear(64,32), then a ModuleList of num_layers copies of Linear(32,32), then output_layer = Linear(32,8). Total parameters?

Recall Solution
  • input_layer: .
  • Each hidden Linear(32,32): . Three of them: .
  • output_layer: .
  • Total .

Level 3 — Analysis

Exercise 3.1

Two students build the "same" 2-hidden-layer net. Student A uses a Python list; Student B uses nn.ModuleList:

# A:
self.layers = [nn.Linear(10,10), nn.Linear(10,10)]
# B:
self.layers = nn.ModuleList([nn.Linear(10,10), nn.Linear(10,10)])

Both call the layers correctly in forward. After training, whose hidden layers actually improved, and what does sum(p.numel() for p in model.parameters()) print for each?

Recall Solution

Each Linear(10,10) has params.

  • Student B registers both → .parameters() sees . The optimizer updates them; they train. ✅
  • Student A's list is invisible → .parameters() returns 0 (no other params exist). The optimizer receives an empty list; those layers keep their random init forever — the forward pass still runs, loss may even wobble due to randomness, but nothing learns. ❌
  • Prints: B → 220, A → 0.

Exercise 3.2

In the parent's ResidualBlock, the forward is out = F(x); out = out + identity; out = relu(out). Suppose a beginner "simplifies" by removing the skip: out = relu(F(x)). Using the gradient rule from the parent, explain what quantity is lost.

Recall Solution

With the skip, the gradient reaching the input is Remove the skip and you lose the : What's lost: the guaranteed " highway." If (dead layer), the skip version still passes gradient , but the stripped version passes — the classic vanishing gradient. The picture:

Figure — Building models with nn.Module

Level 4 — Synthesis

Exercise 4.1

Design a module MLPBlock(dim) whose forward is where is ReLU, maps (with bias) and maps (with bias) — a transformer-style feed-forward block with a residual. Write it, then compute the parameter count for .

Recall Solution
class MLPBlock(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.fc1 = nn.Linear(dim, 4 * dim)   # W1
        self.fc2 = nn.Linear(4 * dim, dim)   # W2
        self.act = nn.ReLU()
    def forward(self, x):
        return x + self.fc2(self.act(self.fc1(x)))

The x + residual adds no parameters (it's just addition). Count for :

  • fc1: .
  • fc2: .
  • Total .

Exercise 4.2

Build TowerNet(dims) that takes a list dims = [d0, d1, ..., dn] and chains Linear(d0,d1), ReLU, Linear(d1,d2), ReLU, ..., Linear(d(n-1), dn) — ReLU between every consecutive pair but not after the last layer. Use the correct container. Then count params for dims = [16, 8, 4, 2].

Recall Solution
class TowerNet(nn.Module):
    def __init__(self, dims):
        super().__init__()
        self.linears = nn.ModuleList(
            [nn.Linear(dims[i], dims[i+1]) for i in range(len(dims) - 1)]
        )
        self.act = nn.ReLU()
    def forward(self, x):
        for i, lin in enumerate(self.linears):
            x = lin(x)
            if i < len(self.linears) - 1:   # no ReLU after the last
                x = self.act(x)
        return x

nn.ModuleList (not a plain list) so all layers register. Count for [16,8,4,2]:

  • Linear(16,8): .
  • Linear(8,4): .
  • Linear(4,2): .
  • Total .

Level 5 — Mastery

Exercise 5.1

You have SimpleClassifier(input_dim, hidden_dim, output_dim) from the parent (three Linear layers fc1: input→hidden, fc2: hidden→hidden, fc3: hidden→output). Derive a closed-form parameter count as a formula in , then verify it reproduces the parent's for .

Recall Solution

Sum the three layers using each: Group it: Plug in :

  • .
  • .
  • .
  • Total . ✅ Matches the parent.

Exercise 5.2

Capacity vs. cost trade-off. For SimpleClassifier, if and are fixed, the count grows fastest in which variable, and why? Using , find the dominant term for large and confirm the growth rate.

Recall Solution

Expand: .

  • The term (from fc2, the hidden-to-hidden layer) dominates for large — parameter count grows quadratically in the hidden width.
  • Why: fc2 connects inputs to outputs, an grid, so doubling roughly quadruples its size, while fc1 and fc3 only grow linearly in .
  • Sanity check the rate: at , ; at , . Doubling multiplied the dominant term by . ✅
Figure — Building models with nn.Module
Recall Quick self-check reveals

Params of nn.Linear(100,50) ::: Params of Linear(784,256)→ReLU→Linear(256,10) ::: DeepNetwork(64,32,3,8) total ::: MLPBlock(8) total ::: TowerNet([16,8,4,2]) total ::: Dominant growth of SimpleClassifier in hidden width ::: quadratic, , from fc2