PyTorch Custom Loss Function

PyTorch's built-in loss functions cover most tasks. When they don't — because your task has a unique objective, class imbalance, or domain-specific requirements — you write your own. Any Python function that takes predictions and targets as tensors and returns a scalar tensor is a valid loss function in PyTorch.

Why Custom Loss Functions Are Needed

Built-in loss: CrossEntropyLoss
  Works for: balanced multi-class classification

What it misses:
  → Class imbalance: 95% samples are class 0, 5% class 1
    → model learns to always predict class 0
  → Ordinal structure: predicting severity (1–5 stars)
    → predicting "5" when truth is "4" should cost less than predicting "1"
  → Task-specific metrics: optimizing directly for F1, IoU, SSIM

Rule: A Loss Function Must Return a Scalar

The loss must be a single number so that .backward() can compute gradients. If your computation produces a tensor, reduce it to a scalar using .mean() or .sum().

Example 1: Weighted Binary Cross-Entropy

When one class is rare, give the model a larger penalty for missing it by assigning it a higher weight.

import torch
import torch.nn.functional as F

def weighted_bce_loss(pred_logits, targets, pos_weight=5.0):
    """
    pred_logits: raw model output (before sigmoid)
    targets:     binary labels (0 or 1)
    pos_weight:  how much more to penalize missing a positive sample
    """
    bce = F.binary_cross_entropy_with_logits(
        pred_logits, targets,
        pos_weight=torch.tensor(pos_weight)
    )
    return bce

# Example
logits  = torch.tensor([2.0, -1.0, 0.5, -2.0])
targets = torch.tensor([1.0,  1.0, 0.0,  0.0])
loss    = weighted_bce_loss(logits, targets, pos_weight=5.0)
print(loss)

Example 2: Focal Loss

Focal loss was designed for object detection with severe class imbalance. It reduces the loss contribution from easy examples (ones the model is already confident about) and focuses training on hard, misclassified examples.

import torch
import torch.nn.functional as F

def focal_loss(pred_logits, targets, alpha=0.25, gamma=2.0):
    """
    alpha: weight for the positive class
    gamma: focusing parameter (higher = more focus on hard examples)
    """
    bce  = F.binary_cross_entropy_with_logits(
               pred_logits, targets, reduction='none')
    probs  = torch.sigmoid(pred_logits)
    p_t    = probs * targets + (1 - probs) * (1 - targets)
    weight = alpha * targets + (1 - alpha) * (1 - targets)
    focal  = weight * ((1 - p_t) ** gamma) * bce
    return focal.mean()

logits  = torch.randn(16)
targets = torch.randint(0, 2, (16,)).float()
loss    = focal_loss(logits, targets)
print(loss)
Focal loss diagram:
  p_t (confidence)   Standard BCE   Focal weight
  0.9 (easy)         low            × (1-0.9)² = 0.01  ← barely contributes
  0.5 (medium)       medium         × (1-0.5)² = 0.25
  0.1 (hard)         high           × (1-0.1)² = 0.81  ← drives most of learning

Example 3: Mean Absolute Error (L1 Loss)

def mae_loss(predictions, targets):
    return torch.abs(predictions - targets).mean()

preds   = torch.tensor([2.5, 3.0, 4.1])
targets = torch.tensor([3.0, 3.0, 4.0])
print(mae_loss(preds, targets))   # tensor(0.2000)

Example 4: Dice Loss for Segmentation

Dice loss is popular in image segmentation tasks where most pixels belong to the background. It directly optimizes the overlap between predicted and true masks.

def dice_loss(pred, target, smooth=1e-6):
    """
    pred:   predicted probabilities after sigmoid, shape [B, H, W]
    target: binary ground truth masks, shape [B, H, W]
    """
    pred   = pred.contiguous().view(-1)
    target = target.contiguous().view(-1)

    intersection = (pred * target).sum()
    dice = (2.0 * intersection + smooth) / (pred.sum() + target.sum() + smooth)
    return 1.0 - dice   # loss = 1 - dice score

pred   = torch.sigmoid(torch.randn(4, 64, 64))
target = torch.randint(0, 2, (4, 64, 64)).float()
print(dice_loss(pred, target))
Dice score = 2 × |A ∩ B| / (|A| + |B|)
  Perfect overlap → Dice = 1 → Loss = 0
  No overlap      → Dice = 0 → Loss = 1

Using a Custom Loss in Training

model     = MySegmentationModel()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)

for images, masks in train_loader:
    optimizer.zero_grad()
    pred = torch.sigmoid(model(images))   # probabilities 0–1
    loss = dice_loss(pred, masks)
    loss.backward()    # works because all operations use PyTorch tensors
    optimizer.step()
    print(f"Loss: {loss.item():.4f}")

Custom Loss as an nn.Module

When your loss function has learnable parameters or you want to use it like a built-in module, implement it as a class inheriting from nn.Module.

class FocalLoss(nn.Module):
    def __init__(self, alpha=0.25, gamma=2.0):
        super().__init__()
        self.alpha = alpha
        self.gamma = gamma

    def forward(self, pred_logits, targets):
        bce   = F.binary_cross_entropy_with_logits(
                    pred_logits, targets, reduction='none')
        probs = torch.sigmoid(pred_logits)
        p_t   = probs * targets + (1 - probs) * (1 - targets)
        wt    = self.alpha * targets + (1 - self.alpha) * (1 - targets)
        return (wt * (1 - p_t) ** self.gamma * bce).mean()

loss_fn = FocalLoss(alpha=0.25, gamma=2.0)
loss    = loss_fn(logits, targets)
loss.backward()   # gradients flow through normally

Combining Multiple Losses

Many tasks benefit from combining losses — for example, image reconstruction tasks often use pixel-level MSE plus a perceptual loss.

def combined_loss(pred, target, pred_features, target_features,
                  mse_weight=1.0, perceptual_weight=0.1):
    mse        = F.mse_loss(pred, target)
    perceptual = F.mse_loss(pred_features, target_features)
    return mse_weight * mse + perceptual_weight * perceptual

Summary

Custom loss functions let you optimize for objectives that built-in losses don't cover. Write them as plain Python functions using PyTorch tensor operations — autograd handles gradient computation automatically. For class imbalance, use weighted BCE or Focal loss. For segmentation, use Dice loss. For regression with outliers, use Huber loss instead of MSE. Wrap custom losses in nn.Module when they have parameters or need to integrate cleanly with the rest of your model code. Always return a scalar (use .mean() or .sum()) so .backward() works correctly.

Leave a Comment

Your email address will not be published. Required fields are marked *