๐ŸŽ“ A1Learner
๐Ÿ”

PyTorch Training an Ensemble

An ensemble is a group of models whose predictions are combined into one answer. This ensemble averages the class probabilities of three separately trained networks.

Scores become probabilities

Softmax turns raw scores into a row of probabilities. The row sums to 1.0, so rows from different models can be averaged fairly.

scores = torch.tensor([[2.0, 1.0, 0.0]])
probs = torch.softmax(scores, dim=1)
print("Probabilities:", [round(v, 2) for v in probs[0].tolist()])
print("Sum:", round(probs.sum().item(), 2))

Average three rows

Two models lean to class 0 and one leans to class 2. The average follows the two, and the lone model is outvoted.

p1 = torch.tensor([0.7, 0.2, 0.1])
p2 = torch.tensor([0.6, 0.3, 0.1])
p3 = torch.tensor([0.1, 0.3, 0.6])
avg = (p1 + p2 + p3) / 3
print("Averaged:", [round(v, 2) for v in avg.tolist()])
print("Ensemble pick:", avg.argmax().item())

One seed per model

Different seeds give different starting weights. Identical code then trains three genuinely different models.

torch.manual_seed(10)
first = nn.Linear(4, 3)
torch.manual_seed(11)
second = nn.Linear(4, 3)
print("Same start:", torch.equal(first.weight, second.weight))

Example

import torch
from torch import nn

def make_shapes(per_class, seed):
    g = torch.Generator().manual_seed(seed)
    rows = torch.arange(16).reshape(16, 1)
    cols = torch.arange(16).reshape(1, 16)
    images = []
    labels = []
    for label in range(3):
        for _ in range(per_class):
            img = torch.zeros(16, 16)
            if label == 0:
                top = int(torch.randint(6, 9, (1,), generator=g))
                img[top:top + 3, 2:14] = 1.0
            elif label == 1:
                dist = ((rows - 7.5) ** 2 + (cols - 7.5) ** 2).sqrt()
                img[(dist >= 4.0) & (dist <= 5.5)] = 1.0
            else:
                img[4:12, 4:6] = 1.0
                img[4:12, 10:12] = 1.0
                img[4:6, 4:12] = 1.0
                img[10:12, 4:12] = 1.0
            img = img + 0.05 * torch.rand(16, 16, generator=g)
            images.append(img)
            labels.append(label)
    x = torch.stack(images).unsqueeze(1)
    y = torch.tensor(labels)
    perm = torch.randperm(len(labels), generator=g)
    return x[perm], y[perm]


class ShapeCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 8, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(8, 16, 3, padding=1),
            nn.ReLU(),
            nn.AdaptiveAvgPool2d(1),
        )
        self.classifier = nn.Linear(16, 3)

    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, 1)
        return self.classifier(x)


def train_model(seed, x, y, epochs=15):
    torch.manual_seed(seed)
    model = ShapeCNN()
    loss_fn = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    for epoch in range(epochs):
        loss = loss_fn(model(x), y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    return model

x_train, y_train = make_shapes(30, 100)
x_test, y_test = make_shapes(15, 200)

models = [train_model(seed, x_train, y_train) for seed in (10, 11, 12)]

probs = []
for i, model in enumerate(models):
    model.eval()
    with torch.no_grad():
        p = torch.softmax(model(x_test), dim=1)
    probs.append(p)
    acc = (p.argmax(dim=1) == y_test).float().mean().item()
    print(f"Model {i + 1} accuracy:", round(acc, 4))

ensemble_probs = (probs[0] + probs[1] + probs[2]) / 3
ensemble_acc = (ensemble_probs.argmax(dim=1) == y_test).float().mean().item()
print("Ensemble accuracy:", round(ensemble_acc, 4))
print("Test images:", len(y_test))

Key points

  • Ensemble: several models combined into one prediction
  • Use torch.softmax to turn each model's scores into probabilities before averaging
  • Each model trains from its own seed, so the models start differently and err differently
  • Averaging probabilities lets confident models count more than hesitant ones
  • The ensemble is one prediction built from three models, not a fourth trained model
  • Ensemble accuracy uses the same argmax rule as a single model's accuracy

In this example, three copies of the shapes network train on the same 90 images. The network is the canonical shapes CNN, with two convolution blocks and a linear classifier. Seeds 10, 11, and 12 give the three models different starting weights. Each model trains for 15 epochs with Adam at rate 0.01. The test set holds 45 fresh shapes images. The first two models score 1.0, and the third scores 0.6667. The averaged probabilities score 1.0, matching the best single model. Averaging also repaired every test image the third model missed, because its partners were right there.

Note

Softmax rows sum to 1.0 per model, so the averaged row is also a probability row

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

How does this ensemble combine its three models?