๐ŸŽ“ A1Learner
๐Ÿ”

PyTorch Class Imbalance

Class imbalance means one class holds far more rows than the other. A model can score high by favouring the big class and ignoring the small one.

The trap in counts

Two hundred rows hold only ten sick cases. A model that finds nobody still scores 0.95 accuracy.

y = torch.tensor([0] * 190 + [1] * 10)
pred = torch.zeros(200, dtype=torch.long)
acc = (pred == y).float().mean().item()
print(f"Accuracy: {acc:.2f}")
print("Sick found:", pred[y == 1].sum().item())

Set the positive weight

pos_weight scales the loss on positive rows. Nineteen matches the ratio of 190 healthy rows to 10 sick rows.

loss_fn = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([19.0]))
print(loss_fn.pos_weight)

Example

import torch
from torch import nn

def run(use_weights):
    torch.manual_seed(0)
    n0, n1 = 190, 10
    x0 = torch.randn(n0, 2) + torch.tensor([-0.5, -0.5])
    x1 = torch.randn(n1, 2) + torch.tensor([0.5, 0.5])
    x = torch.cat([x0, x1])
    y = torch.cat([torch.zeros(n0), torch.ones(n1)]).reshape(-1, 1)
    order = torch.randperm(len(x))
    x, y = x[order], y[order]
    torch.manual_seed(1)
    model = nn.Linear(2, 1)
    if use_weights:
        loss_fn = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([19.0]))
    else:
        loss_fn = nn.BCEWithLogitsLoss()
    opt = torch.optim.Adam(model.parameters(), lr=0.05)
    for epoch in range(40):
        loss = loss_fn(model(x), y)
        opt.zero_grad()
        loss.backward()
        opt.step()
    model.eval()
    with torch.no_grad():
        pred = (model(x) > 0).float()
    sick = y == 1
    recall = (pred[sick] == 1).float().mean().item()
    acc = (pred == y).float().mean().item()
    return acc, recall

a0, r0 = run(False)
a1, r1 = run(True)
print(f"No weights:   accuracy {a0:.2f}, sick recall {r0:.2f}")
print(f"With weights: accuracy {a1:.2f}, sick recall {r1:.2f}")

Key points

  • Minority class: the class with far fewer rows
  • A model that always picks the majority class still scores the majority share as accuracy
  • Class weight: a multiplier that makes minority mistakes cost more in the loss
  • Use pos_weight in BCEWithLogitsLoss to weight the positive class
  • Recall on the minority class shows whether the weighting worked

In this example, one function trains the same problem twice. The data holds 190 healthy rows and 10 sick rows in two overlapping clouds. A shuffle mixes the classes, and the seed resets so both runs start alike. The first run uses a plain BCEWithLogitsLoss. It reaches accuracy 0.94 and finds only 0.20 of the sick rows. The second run sets pos_weight to 19, the class ratio. Its accuracy drops to 0.84, and its sick recall climbs to 0.90. The two lines print the trade directly.

Note

Weighting trades some overall accuracy for minority recall, and the closing example shows both numbers

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

What does pos_weight do in BCEWithLogitsLoss?