Loss functions define the scalar objective a PyTorch model tries to minimize. Built-in criteria cover common tasks, but domain-specific penalties often need extra weighting, masking, or piecewise math that still has to remain inside autograd.
Subclassing torch.nn.Module keeps the custom criterion usable like a built-in loss. Instantiate it once, call it with predictions and targets, and pass the returned scalar tensor to loss.backward().
A small CPU smoke script can prove the criterion before it moves into a training project. In the smoke workload, under-predictions receive a larger Huber-style penalty, then backward() fills a gradient and optimizer.step() changes a model weight.
Related: How to create a custom nn.Module in PyTorch
Related: How to run a training loop in PyTorch
Related: How to debug NaN loss in PyTorch
Steps to create a custom PyTorch loss function:
- Create a smoke script with a custom loss module.
- custom_loss_demo.py
import torch from torch import nn class WeightedHuberLoss(nn.Module): def __init__(self, delta=0.5, under_prediction_weight=2.0): super().__init__() self.delta = delta self.under_prediction_weight = under_prediction_weight def forward(self, prediction, target): error = prediction - target abs_error = error.abs() quadratic = torch.minimum(abs_error, torch.full_like(abs_error, self.delta)) linear = abs_error - quadratic huber = 0.5 * quadratic.pow(2) + self.delta * linear weights = torch.where( error < 0, torch.full_like(error, self.under_prediction_weight), torch.ones_like(error), ) return (weights * huber).mean() torch.manual_seed(19) features = torch.tensor( [ [0.0, 0.5], [1.0, -0.5], [2.0, 1.0], [-1.0, 1.5], ], dtype=torch.float32, ) targets = torch.tensor([[0.4], [1.0], [2.4], [-0.2]], dtype=torch.float32) model = nn.Sequential( nn.Linear(2, 4), nn.Tanh(), nn.Linear(4, 1), ) loss_fn = WeightedHuberLoss(delta=0.5, under_prediction_weight=2.5) optimizer = torch.optim.SGD(model.parameters(), lr=0.05) optimizer.zero_grad(set_to_none=True) prediction = model(features) loss = loss_fn(prediction, targets) loss.backward() gradient_norms = [ parameter.grad.detach().norm() for parameter in model.parameters() if parameter.grad is not None ] gradient_norm = torch.linalg.vector_norm(torch.stack(gradient_norms), 2) first_layer_grad_present = model[0].weight.grad is not None first_layer_before = model[0].weight.detach().clone() optimizer.step() weight_delta = (model[0].weight.detach() - first_layer_before).abs().max() print(f"torch_version={torch.__version__}") print(f"custom_loss={loss.item():.6f}") print(f"gradient_norm={gradient_norm.item():.6f}") print(f"first_layer_grad_present={first_layer_grad_present}") print(f"optimizer_step_changed_weight={weight_delta.item() > 0}")
forward() uses only torch tensor operations, so autograd can trace the loss back through the model. torch.where() applies the larger penalty only where the prediction is below the target.
- Run the custom loss smoke script.
$ python custom_loss_demo.py torch_version=2.12.1+cu130 custom_loss=1.338794 gradient_norm=1.481275 first_layer_grad_present=True optimizer_step_changed_weight=True
- Check the gradient and update lines.
gradient_norm=1.481275 and first_layer_grad_present=True confirm loss.backward() reached model parameters. optimizer_step_changed_weight=True confirms the gradient produced an optimizer update.
- Copy the loss class into the project code.
class WeightedHuberLoss(nn.Module): def __init__(self, delta=0.5, under_prediction_weight=2.0): super().__init__() self.delta = delta self.under_prediction_weight = under_prediction_weight def forward(self, prediction, target): error = prediction - target abs_error = error.abs() quadratic = torch.minimum(abs_error, torch.full_like(abs_error, self.delta)) linear = abs_error - quadratic huber = 0.5 * quadratic.pow(2) + self.delta * linear weights = torch.where( error < 0, torch.full_like(error, self.under_prediction_weight), torch.ones_like(error), ) return (weights * huber).mean()
- Instantiate the custom loss beside the optimizer.
model = MyModel() optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4) loss_fn = WeightedHuberLoss(delta=0.5, under_prediction_weight=2.5)
- Use the custom loss in the training step.
for batch_features, batch_targets in train_loader: optimizer.zero_grad(set_to_none=True) predictions = model(batch_features) loss = loss_fn(predictions, batch_targets) loss.backward() optimizer.step()
Keep predictions, batch_targets, and any tensors created inside forward() on the same device and dtype.
Related: How to zero gradients in PyTorch - Keep non-tensor conversions out of forward().
Do not call .detach(), .item(), or numpy() on values that still need gradients before returning the loss. Those conversions remove data from the autograd graph and can leave model parameters without gradients.
- Remove the temporary smoke script.
$ rm custom_loss_demo.py
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.