build_resnet — fill in two holes
In the last step you saw what has to be done to resnet18. Now you write it as
a function — and fill in the two missing pieces yourself.
The algorithm
model ← resnet18 with pretrained weights
for every parameter p in the model:
switch off its gradients # freeze
if finetune_last_block:
for every parameter p of model.layer4:
switch gradients on # unlock the last block
model.fc ← a new Linear(512 → num_classes) # new head
return model
The order matters. First you freeze everything, then unlock layer4, and
only then replace the head. A freshly created layer is trainable by default —
you do nothing for it. If you replaced the head before freezing, the freezing
loop would freeze it too.
Your job
Create transfer.py and test_transfer.py (see the right panel).
transfer.py runs but is wrong — two places, HOLE 1 and HOLE 2:
- HOLE 1 — freezing one parameter. Right now there is a
pass, so nothing is frozen. You need one line withp.requires_grad. - HOLE 2 — the new head. Right now
model.fcstays at 1,000 classes; replace it with a singlenn.Linear. Take the input size from the old layer:model.fc.in_features.
Unlocking for finetune_last_block is already written.
test_transfer.py is the verdict. It does not download weights: it swaps
resnet18 for an untrained network of the same size. It tests not accuracy but
the wiring: what is frozen, how big the head is, what is unlocked.
Run it before filling anything in
$ python -m unittest test_transfer
FFF.
======================================================================
FAIL: test_finetuning_unfreezes_layer4_and_nothing_else (...)
AssertionError: True != False : conv1.weight
======================================================================
FAIL: test_the_backbone_is_frozen (...)
AssertionError: Lists differ: ['conv1.weight', 'bn1.weight', 'bn1.bias', ...] != []
...
First list contains 60 additional elements.
======================================================================
FAIL: test_the_head_has_one_output_per_class (...)
AssertionError: Tuples differ: (1000, 512) != (4, 512)
----------------------------------------------------------------------
Ran 4 tests in 0.4s
FAILED (failures=3)
(Shortened.) Read it: 60 parameter tensors of the backbone are still trainable though they should be frozen, and the head has 1,000 outputs, not 4. The one test that passes ("the new head is trainable") passes only because you have not frozen anything yet.
Fill in both holes and run again: you should see OK.
When it does not pass
True != False : conv1.weight— freezing did not reach every parameter. Did you writeTrueinstead ofFalse, orp.detach()(which returns a copy and does not changep)?Tuples differ: (512, 4) != (4, 512)— thenn.Lineararguments are swapped. First is the input (512), second the output (the class count).False != True : fc.weight(and the head stays at 1,000) — you wrotemodel.fc.out_features = num_classes. That is only a number on the object; the weight table stays the same. A new layer has to be created. You also see a side effect: the old head had been frozen, so the "new head" is not trainable either.classifier.weight—classifieris the head of other networks (e.g. VGG); inresnet18it is calledfc. Different networks, different names.