๐ŸŽ“ A1Learner
๐Ÿ”

PyTorch Writing a Custom Layer

A custom layer is a module that wraps new maths in the standard module shape. It stores its learnable numbers in __init__, so an optimizer can find and adjust them.

A layer is a class

The class extends nn.Module and builds its parts in __init__. Temperature is one learnable number that this layer divides its input by. nn.Parameter wraps a plain tensor as that learnable number.

class TemperatureScale(nn.Module):
    def __init__(self):
        super().__init__()
        self.temperature = nn.Parameter(torch.tensor(1.0))

    def forward(self, x):
        return x / self.temperature

Registration puts the number on the list

Assigning an nn.Parameter to self in __init__ registers it. Registered parameters appear in named_parameters() with their names.

layer = TemperatureScale()
for name, param in layer.named_parameters():
    print(name, param.item(), param.requires_grad)
print("Parameter count:", sum(p.numel() for p in layer.parameters()))

The layer divides by its temperature

The temperature is set to 2.0 by hand here. Every score comes out halved.

with torch.no_grad():
    layer.temperature.fill_(2.0)
scores = torch.tensor([4.0, 1.0, -2.0])
print(layer(scores).tolist())

Example

import torch
from torch import nn


class TemperatureScale(nn.Module):
    def __init__(self):
        super().__init__()
        self.temperature = nn.Parameter(torch.tensor(1.0))

    def forward(self, x):
        return x / self.temperature


torch.manual_seed(0)
x = torch.tensor([[2.0, 0.0], [0.0, 2.0], [3.0, 1.0], [1.0, 3.0],
                  [2.5, 0.5], [0.5, 2.5], [3.0, 3.0], [0.0, 0.0]])
y = torch.tensor([0, 1, 0, 1, 0, 1, 0, 1])

model = nn.Sequential(nn.Linear(2, 2), TemperatureScale())
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)

with torch.no_grad():
    start_loss = loss_fn(model(x), y).item()
    start_temp = model[1].temperature.item()

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

with torch.no_grad():
    final_loss = loss_fn(model(x), y).item()
    final_temp = model[1].temperature.item()
    correct = (model(x).argmax(dim=1) == y).sum().item()

print("Start temperature:", round(start_temp, 3))
print("Start loss:", round(start_loss, 4))
print("Final temperature:", round(final_temp, 3))
print("Final loss:", round(final_loss, 4))
print("Correct:", correct, "of", len(y))

Key points

  • Custom layer: a module class that defines new maths in forward
  • Use nn.Parameter to wrap a tensor as a learnable number on a module
  • Registration: storing a parameter on self inside __init__
  • An optimizer finds registered parameters through model.parameters()
  • A custom layer is a module, so it drops into nn.Sequential like a built-in layer

In this example, the TemperatureScale layer sits after a Linear layer inside nn.Sequential. The data holds eight rows with two numbers each and a class label per row. The temperature starts at 1.0, so the scores pass through unchanged at first. Adam trains the whole model for 40 steps at a learning rate of 0.05. The loss falls from 0.9841 to 0.0029. The temperature itself moves to 0.638, which sharpens every score the Linear layer makes. Dividing by a smaller number stretches the scores apart, so the right class wins by more. The final line shows all eight rows classified correctly.

Note

A plain float stored on self is not a parameter, and no optimizer will adjust it

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

Where must a learnable number be stored so an optimizer finds it?