class LinearRegression(d2l.Module):
"""The linear regression model implemented with high-level APIs."""
def __init__(self, lr):
super().__init__()
self.save_hyperparameters()
self.net = nn.LazyLinear(1)Dive into Deep Learning · §2.5
The same model, the concise way
batteries-included layers, losses, and optimizers replace the hand-rolled parts.
Motivation
Last section we wrote every piece by hand: the weight vector, the forward pass, the squared error, the update step.
Those pieces are so universal that frameworks ship them, tuned and tested. We swap each one for its built-in counterpart:
Layer replaces w, b · loss replaces our squared error · optimizer replaces the update loop.
| By hand | Built-in |
|---|---|
w, b |
a layer |
| MSE math | a loss |
| update step | an optimizer |
01
The Model
a single linear layer
The Model
What we hand-rolled as w, b, and a matrix–vector product, every framework ships as a fully connected layer: each input wired to the one output: exactly the picture of linear regression.
The layer owns its parameters. We no longer allocate them, initialize them, or even know their shapes ahead of time.
The Model
LazyLinear(1) is the whole model. The lazy variant defers the input dimension until the first forward pass. Initialize its parameters only after that first pass:
Lazy shape inference pays off in deep nets (conv layers, variable-length sequences) where the input size is tedious to work out.
The Model
forward just calls the layer. All the matrix–vector arithmetic we wrote by hand now lives inside it:
02
Loss & Optimizer
two more pieces, off the shelf
Loss & Optimizer
The framework’s MSE replaces our hand-written squared error:
It omits the \tfrac{1}{2} factor we used by hand, and averages over the minibatch by default.
Loss & Optimizer
The update loop becomes a single optimizer object, handed the parameters and the learning rate:
The same optim/Trainer family also gives momentum, Adam, and more by swapping one line.
03
Training
the scaffold never changed
Training
Our Trainer, Module, and DataModule from the object-oriented-design section don’t care that the model is now a built-in layer.
The training loop is identical to the from-scratch version.
Training
Same synthetic data, same ten epochs, same fit call as the linear-regression-from-scratch section:
model = LinearRegression(lr=0.03)
data = d2l.SyntheticRegressionData(w=d2l.tensor([2, -3.4]), b=4.2)
# Materialize lazy parameters before replacing their default initialization.
model(data.X[:1])
with torch.no_grad():
model.net.weight.normal_(0, 0.01)
model.net.bias.fill_(0)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)Nothing about the training run can tell the two implementations apart: only the amount of code we wrote changed.
Training · payoff
They no longer hang off our class as self.w, self.b; they live inside the layer, so get_w_b reaches through net:
error in estimating w: tensor([-0.0003, 0.0008])
error in estimating b: tensor([0.0004])
Same verdict as the linear-regression-from-scratch section: the true \mathbf{w}^* = [2,-3.4], b^* = 4.2 recovered to a few 10^{-4}. The built-in pieces really do compute the same thing our hand-rolled ones did.
Wrap-up
w, b; a built-in loss and optimizer replace the rest.Module / Trainer / DataModule scaffold is unchanged; only the model’s internals got shorter.