Neural Network Abstractions: Module, Linear, Sequential
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Build a Value graph and propagate a scalar loss backward.
- Use Python attributes, lists, and nested objects.
What you will practice
- Enumerate each trainable identity once despite aliases.
- Trace a Linear forward pass and input gradient using output-row weight storage.
- Distinguish evaluation mode from disabling gradients.
In this lesson
Lesson
4 min readNeural Network Abstractions: Module, Linear, Sequential
Course role: v1 is a conceptual bridge with compact scalar exercises. For the cumulative implementation sequence, numerical boundary cases, and learned multi-head projections, continue with deep learning v2. The v1 transformer exercise uses a single attention head even though its API accepts
num_heads.
Goal
Replace ad-hoc classes with a small framework that matches PyTorch's mental model:
Modulebase class with automatic parameter trackingLinearlayer and common activationsSequentialcontainer for composition
Prerequisites: Autograd module.
1) The Module contract
A Module should:
- Be callable via
__call__ -> forward - Track parameters (
Values) automatically - Track nested modules recursively
- Support train/eval mode flags
Minimal state:
_parameters: dict[str, Value]_modules: dict[str, Module]training: bool
2) Auto-registration via __setattr__
When a user assigns attributes, intercept and register:
class Module:
def __init__(self):
object.__setattr__(self, "_parameters", {})
object.__setattr__(self, "_modules", {})
object.__setattr__(self, "training", True)
def __setattr__(self, name, value):
if isinstance(value, Value):
self._parameters[name] = value
elif isinstance(value, Module):
self._modules[name] = value
elif isinstance(value, (list, tuple)):
self._register_list(name, value)
object.__setattr__(self, name, value)
Nested lists/tuples should register recursively (e.g., list of layers).
3) Parameter traversal
A parameter may have several names but is still one trainable scalar. Traverse registered parameters and children while retaining sets of visited parameter and module identities. Yield each parameter only on its first visit; named_parameters uses that first path as its name. Equality of numeric data is not enough: two distinct Value(0) objects are different parameters.
Work a shared-layer example: a parent stores Linear(2,3) under both layer and a one-element layers list. The child has 6 weights and 3 biases, so both enumerations must return 9 unique scalars, not 18. Plain recursive concatenation of both attribute paths would cause an SGD loop to update each twice even if its gradient were correct.
Sketch the traversal after attempting it
Maintain seen_modules and seen_parameters. On visiting a new module, emit its unseen own parameters in insertion order, then recurse through children. Retain these sets for the whole traversal rather than starting new ones in each child. parameters() can discard the names from named_parameters().
4) Linear layer (fully connected)
Shapes:
- input
x: lengthin_features - weight:
(out_features, in_features) - bias:
(out_features,)orNone
Forward:
y_i = sum_j weight[i][j] * x[j] + bias[i]
The initialization formulas distinguish fan-in from Glorot/Xavier conventions. Initialization here is uniform in [-1/sqrt(in_features), +1/sqrt(in_features)], a fan-in convention. Glorot/Xavier uniform instead uses a bound based on both fan-in and fan-out (with gain 1, sqrt(6/(fan_in+fan_out))).
5) Sequential container
Sequential stores modules in order and feeds outputs through each:
class Sequential(Module):
def __init__(self, *modules):
super().__init__()
self.module_list = list(modules)
for i, m in enumerate(modules):
setattr(self, f"layer_{i}", m)
def forward(self, x):
for m in self.module_list:
x = m(x)
return x
6) Activation modules
Each activation maps a list of Value to a list of Value:
Tanh:xi.tanh()ReLU:xi.relu()Sigmoid: use1/(1+exp(-x))for nonnegative x andexp(x)/(1+exp(x))for negative x, preserving Value operations. This avoids exponent overflow at finite logits such as -1000.
These have no parameters but still behave like Modules.
7) Train/Eval mode
def train(self, mode=True):
self.training = mode
for m in self._modules.values():
m.train(mode)
return self
def eval(self):
return self.train(False)
Used by layers like dropout/batchnorm (not implemented here, but the flag is part of the API). Calling eval() changes this mode flag; it does not disable Value graph construction or backward.
Key takeaways
Modulecentralizes parameter tracking and nesting.__setattr__is the hook that makes it automatic.Linear+ activations +Sequentialare enough for many models.- Train/eval mode is an API contract, even if unused now.
Next: embeddings and positional encodings for sequence models.
Reconstruct a layer before composing it
Set a two-output layer's weight rows to [[1,2],[-1,3]], biases to [0.5,-0.5], and input to [2,1]. Calculate each output and the gradient of their sum with respect to the input before revealing the check.
Check the linear trace
Outputs are [4.5,0.5]. The sum's input gradient is [1+(-1),2+3]=[0,5]. Each weight receives its input coordinate's contribution; do not create fresh Value objects from weight.data during forward, or registration and the graph will disagree.
The ML pack's batched X@W stores weight columns per output; this scalar layer stores rows per output. Translate the storage layout explicitly. Then implement a new three-layer composition and predict its unique parameter count before running it.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.
- Step 1
Neural Network Module: The Foundation of DL Abstractions
- Step 2
Linear Layer and Sequential: Building Architectures
- Step 3
Neural Network Abstractions Checkpoint
Test your understanding of Module, Linear, and Sequential.