Loss functions turn prediction errors into the scalar value that a model minimizes. When under-prediction has a higher cost than over-prediction, a symmetric built-in criterion cannot represent that domain rule by itself.
A loss composed from PyTorch tensor operations can inherit from nn.Module and rely on autograd for its backward graph. A separate torch.autograd.Function is unnecessary unless the calculation uses operations that autograd cannot record or needs a custom gradient rule.
An asymmetric mean-squared error makes the behavior visible with two equal-sized errors: the under-prediction receives a multiplier while the over-prediction does not. Numerical gradient checking and a backward pass through nn.Linear then confirm that the criterion supplies finite model-parameter gradients.
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
import torch from torch import nn from torch.autograd import gradcheck class AsymmetricMSELoss(nn.Module): def __init__(self, under_prediction_weight=2.0): super().__init__() if under_prediction_weight < 1: raise ValueError("under_prediction_weight must be at least 1") self.under_prediction_weight = under_prediction_weight
The constructor keeps the penalty factor as module state and rejects values that would make under-prediction cheaper than ordinary mean-squared error.
def forward(self, prediction, target): squared_error = (prediction - target).square() weights = torch.where( prediction < target, self.under_prediction_weight, 1.0, ) return (weights * squared_error).mean()
torch.where() selects the multiplier element by element, and mean() reduces the weighted errors to the scalar loss required by backward().
if __name__ == "__main__": loss_fn = AsymmetricMSELoss(under_prediction_weight=3.0) under_prediction_loss = loss_fn(torch.tensor([0.0]), torch.tensor([1.0])) over_prediction_loss = loss_fn(torch.tensor([2.0]), torch.tensor([1.0]))
Both predictions are one unit from the target. The configured multiplier should therefore make under_prediction_loss three times over_prediction_loss.
check_prediction = torch.tensor( [0.25, 1.75], dtype=torch.float64, requires_grad=True, ) check_target = torch.tensor([1.0, 1.0], dtype=torch.float64) gradient_check = gradcheck(loss_fn, (check_prediction, check_target)) model = nn.Linear(1, 1, bias=False, dtype=torch.float64) features = torch.tensor([[1.0], [2.0]], dtype=torch.float64) targets = torch.tensor([[1.0], [2.0]], dtype=torch.float64) training_loss = loss_fn(model(features), targets) training_loss.backward() parameter_gradient = model.weight.grad assert under_prediction_loss > over_prediction_loss assert gradient_check assert parameter_gradient is not None assert torch.isfinite(parameter_gradient).all() print(f"under_prediction_loss={under_prediction_loss.item():.6f}") print(f"over_prediction_loss={over_prediction_loss.item():.6f}") print(f"gradient_check={gradient_check}") print(f"parameter_gradient_finite={bool(torch.isfinite(parameter_gradient).all())}")
gradcheck() is designed for double-precision inputs and compares finite-difference gradients with the analytical gradients from autograd. The chosen values stay away from the branch boundary where prediction equals target.
$ python3 asymmetric_mse_loss.py under_prediction_loss=3.000000 over_prediction_loss=1.000000 gradient_check=True parameter_gradient_finite=True
The two loss values confirm the asymmetric penalty. The final two lines confirm that the loss passes numerical gradient checking and propagates a finite gradient to a model parameter.