๐ŸŽ“ A1Learner
๐Ÿ”

PyTorch Parameter Groups

Parameter groups are separate bundles of parameters inside one optimizer. Each group carries its own settings, like its own learning rate.

One rate for everything

One plain optimizer call gives every parameter the same rate. A fresh head often needs bigger steps than a settled body.

optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

Two groups, two rates

A list of dicts replaces the plain parameter list. Each dict names its parameters and its own rate.

import torch

torch.manual_seed(0)
body = torch.nn.Linear(2, 4)
head = torch.nn.Linear(4, 1)
optimizer = torch.optim.SGD(
    [
        {"params": body.parameters(), "lr": 0.01},
        {"params": head.parameters(), "lr": 0.1},
    ]
)
print([group["lr"] for group in optimizer.param_groups])

What each group holds

Each group here holds two tensors. One tensor is the layer's weight, and one is its bias.

import torch

torch.manual_seed(0)
body = torch.nn.Linear(2, 4)
head = torch.nn.Linear(4, 1)
optimizer = torch.optim.SGD(
    [
        {"params": body.parameters(), "lr": 0.01},
        {"params": head.parameters(), "lr": 0.1},
    ]
)
print([len(group["params"]) for group in optimizer.param_groups])

Example

import torch

torch.manual_seed(0)


class TwoPartModel(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.body = torch.nn.Linear(2, 4)
        self.head = torch.nn.Linear(4, 1)

    def forward(self, x):
        return self.head(torch.relu(self.body(x)))


x = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0],
                  [2.0, 1.0], [1.0, 2.0], [2.0, 2.0], [3.0, 1.0]])
y = torch.tensor([[1.0], [3.0], [4.0], [6.0],
                  [9.0], [8.0], [11.0], [12.0]])
model = TwoPartModel()
loss_fn = torch.nn.MSELoss()
optimizer = torch.optim.SGD(
    [
        {"params": model.body.parameters(), "lr": 0.01},
        {"params": model.head.parameters(), "lr": 0.1},
    ]
)

body_before = torch.cat([p.detach().clone().flatten() for p in model.body.parameters()])
head_before = torch.cat([p.detach().clone().flatten() for p in model.head.parameters()])

for step in range(50):
    optimizer.zero_grad()
    loss = loss_fn(model(x), y)
    loss.backward()
    optimizer.step()

body_after = torch.cat([p.detach().flatten() for p in model.body.parameters()])
head_after = torch.cat([p.detach().flatten() for p in model.head.parameters()])
body_change = (body_after - body_before).abs().max().item()
head_change = (head_after - head_before).abs().max().item()

print(f"Group 0 lr: {optimizer.param_groups[0]['lr']}")
print(f"Group 1 lr: {optimizer.param_groups[1]['lr']}")
print(f"Final loss: {loss.item():.4f}")
print(f"Body max change: {body_change:.4f}")
print(f"Head max change: {head_change:.4f}")

Key points

  • Param group: a dict with a params list and that group's settings
  • Use a list of dicts to create an optimizer with several groups
  • Use optimizer.param_groups to read the groups back in order
  • Body: the main part of a model, and head: the final layer that gives the answer
  • Groups let one optimizer train different parts at different speeds

In this example, a small model has a body layer and a head layer. The data follows the rule 3 times the first input, plus 2 times the second, plus 1. One SGD optimizer holds two groups with rates 0.01 and 0.1. The model trains for 50 full-batch steps. The final loss prints as 0.0196. The head's biggest change is 3.1800, and the body's is 0.9576. The head moved more in this run, and its rate was ten times bigger.

Note

Groups keep their creation order, and param_groups[0] is the first group built

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

How does one optimizer give a model's head a bigger learning rate than its body?