Spatial Transformer Networks: Learned Spatial Invariance

151-spatial-transformer-network

Spatial Transformer Network: Complete Guide — Learned Spatial Invariance

Summary

Spatial Transformer Networks (STNs) are one of the most elegant ideas in deep learning to emerge in recent years. Introduced in 2015 by Max Jaderberg, Karen Simonyan, Andrew Zisserman, and Koray Kavukcuoglu, they offer a fundamental solution to a long-standing problem in computer vision: spatial invariance.

A classic convolutional neural network learns to recognize visual patterns very well, but it is terribly vulnerable to geometric transformations. The same digit “7” rotated by thirty degrees can be misclassified by a CNN that has never seen a “7” at that angle. The Spatial Transformer Network solves this problem by learning to actively transform the input data before processing it — in a fully differentiable manner, directly within the network’s training loop.

Unlike data augmentation techniques that merely show varied examples to the network, the STN teaches itself to adapt dynamically. Each image is transformed on a case-by-case basis, according to what the network itself deems optimal to recognize it. It is a genuinely learned spatial invariance, not a passive augmentation of training data.

In this complete guide, we will explore the mathematical principle of the Spatial Transformer Network, the intuition behind its operation, a detailed implementation in PyTorch, as well as its practical use cases.

Mathematical Principle

The Spatial Transformer Network consists of three functional modules that work in sequence: the Localization Network, the Grid Generator, and the Sampler. Together, these three components form a fully differentiable block that can be inserted anywhere in an existing convolutional network.

Overall Architecture

$$STN = \text{Localization Network} + \text{Grid Generator} + \text{Sampler}$$

Each module plays a precise role in transforming the input image. Let’s look at them in detail.

1. Localization Network

The localization network is itself a small neural network that examines the input image $X$ and produces the transformation parameters $\theta$. We denote this operation as:

$$\theta = f_{loc}(X)$$

where $\theta$ is a $2 \times 3$ affine transformation matrix:

$$\theta = \begin{bmatrix} a_{11} & a_{12} & b_1 \ a_{21} & a_{22} & b_2 \end{bmatrix}$$

The six parameters $a_{11}, a_{12}, a_{21}, a_{22}, b_1, b_2$ encode the geometric transformations to apply to the image:

  • $a_{11}, a_{22}$: horizontal and vertical scaling factors (zoom in/out)
  • $a_{12}, a_{21}$: shear and rotation components
  • $b_1, b_2$: horizontal and vertical translations

For example, a pure rotation by angle $\phi$ is written as:

$$\theta_{rotation} = \begin{bmatrix} \cos(\phi) & -\sin(\phi) & 0 \ \sin(\phi) & \cos(\phi) & 0 \end{bmatrix}$$

A translation of $(t_x, t_y)$ in pixels is written as:

$$\theta_{translation} = \begin{bmatrix} 1 & 0 & t_x/H \ 0 & 1 & t_y/W \end{bmatrix}$$

The localization network typically consists of convolutional layers followed by fully connected layers that ultimately produce the six values of $\theta$. The crucial trick is that the last layer of the localizer uses no activation function — the transformation parameters must be able to take any real value to represent all possible transformations.

2. Grid Generator

The grid generator uses the matrix $\theta$ to create a coordinate grid that defines how the pixels of the output image will be mapped to the pixels of the input image.

For each position $(x_i^t, y_i^t)$ in the target image (the transformed image we want to produce), we compute the corresponding source position $(x_i^s, y_i^s)$ in the input image:

$$\begin{bmatrix} x_i^s \ y_i^s \end{bmatrix} = \begin{bmatrix} a_{11} & a_{12} & b_1 \ a_{21} & a_{22} & b_2 \end{bmatrix} \begin{bmatrix} x_i^t \ y_i^t \ 1 \end{bmatrix}$$

Which is equivalent to the coordinate equations:

$$x_i^s = a_{11} \cdot x_i^t + a_{12} \cdot y_i^t + b_1$$
$$y_i^s = a_{21} \cdot x_i^t + a_{22} \cdot y_i^t + b_2$$

This operation is performed for every pixel of the output grid. The result is a grid of coordinates $(x^s, y^s)$ of the same dimension as the target image, where each cell indicates “where to fetch the value of this pixel from the source image.”

This approach — sampling starting from the target grid coordinates rather than the source image coordinates — is called inverse warping. It guarantees that each output pixel receives exactly one value, with no holes or overlaps.

3. Sampler

The grid $(x_i^s, y_i^s)$ contains continuous (floating-point) coordinates, but the pixels of the input image are on a discrete grid of integer coordinates. The sampler must resolve this problem.

Bilinear interpolation is used, which computes the value of a target pixel as a weighted average of the four nearest pixels in the source image:

$$V_i^c = \sum_k \max(0, 1 – |x^s – x_k|) \cdot \max(0, 1 – |y^s – y_k|) \cdot U_{k}^c$$

where:
– $V_i^c$ is the value of channel $c$ at pixel $i$ of the output
– $U_{k}^c$ is the value of channel $c$ at source pixel $k$
– $x_k$ and $y_k$ are the integer coordinates of neighboring pixels
– $\max(0, 1 – |x^s – x_k|)$ is the linear weight based on horizontal distance
– $\max(0, 1 – |y^s – y_k|)$ is the linear weight based on vertical distance

Concretely, if a source coordinate $(x^s, y^s)$ falls halfway between pixels $(2,3)$, $(3,3)$, $(2,4)$, and $(3,4)$, the sampler interpolates between these four pixels with weights of $0.25$ each. If the coordinate is closer to pixel $(3,3)$, the latter will receive the highest weight.

Full Differentiability

The crucial point — and what distinguishes the Spatial Transformer Network from a simple image transformation applied as preprocessing — is that the entire operation is differentiable with respect to $\theta$.

The grid generator is a matrix multiplication, hence differentiable. The sampler is bilinear interpolation, also differentiable. Therefore, the gradient of the loss function can propagate through the entire STN block, all the way back to the localization network. This means that $f_{loc}$ learns which transformations to apply based on the final task, without explicit supervision on the shape of the transformations.

The STN integrates into any CNN as a simple interchangeable module. It can be inserted between two convolutional layers, at the beginning of the network, or even in multiple locations.

Intuition

To understand why the Spatial Transformer Network is so powerful, you first need to understand the problem faced by classical CNNs when dealing with transformations.

A classical convolutional network learns by heart that an object in the top-left of the image is different from the same object in the bottom-right. Convolutional filters detect patterns at specific positions in the source image. Although convolution itself possesses a form of translation invariance (a filter detects a pattern regardless of its local position), the pooling and fully connected layers downstream anchor results to specific spatial regions of the network.

This means that a “6” written in the top-left and the same “6” written in the bottom-right activate different neurons in the deep layers. The network must therefore either have seen both cases during training (which is impossible for all possible transformations) or generalize imperfectly.

The Spatial Transformer acts like eyes that automatically re-center the image on the area of interest before passing it to the brain of the network. This is exactly what our eyes do when we read: they make rapid saccades to place each word in the center of the fovea, the sharp-vision area of the retina.

Think of it another way: it’s like a professional photographer who crops and straightens the horizon before sending the photo to the editor. The downstream network always receives a “clean” image, centered, with the correct orientation. It doesn’t need to handle variations in position, rotation, or scale because the STN takes care of it for the whole system.

This analogy reveals something profound: the Spatial Transformer Network adds a capacity for spatial awareness that CNNs lack. The photographer’s eyes analyze the scene, find the interesting subject, and adjust the framing accordingly. Similarly, the localization network analyzes the image, finds the relevant area, and computes the optimal transformation parameters.

Implementation with Python and PyTorch

Here is a complete implementation of a Spatial Transformer Network integrated into a CNN for classifying MNIST digits, with training on images perturbed by random translations and rotations.

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np


class Localizer(nn.Module):
    """Localization network that predicts affine transformation parameters."""

    def __init__(self, hidden_dim=64, num_fc_layers=3):
        super().__init__()

        # Convolutional layers for feature extraction
        self.conv1 = nn.Conv2d(1, 16, kernel_size=5, padding=2)
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(16)
        self.bn2 = nn.BatchNorm2d(32)
        self.pool = nn.MaxPool2d(2, 2)

        # Size calculation after convolutions + pooling
        # Input 28x28 -> conv(28) -> pool(14) -> conv(14) -> pool(7)
        self.flattened_size = 32 * 7 * 7

        # Fully connected layers
        fc_layers = []
        in_dim = self.flattened_size
        for i in range(num_fc_layers - 1):
            fc_layers.append(nn.Linear(in_dim, hidden_dim))
            fc_layers.append(nn.ReLU())
            in_dim = hidden_dim
        self.fc = nn.Sequential(*fc_layers)

        # Final layer to theta parameters (2x3 = 6 values)
        # No activation! We want a linear output
        self.fc_loc_final = nn.Linear(hidden_dim, 6)

        # Initialization: identity by default to start
        with torch.no_grad():
            self.fc_loc_final.weight.zero_()
            self.fc_loc_final.bias.copy_(
                torch.tensor([1.0, 0.0, 0.0, 0.0, 1.0, 0.0])
            )

    def forward(self, x):
        x = self.pool(F.relu(self.bn1(self.conv1(x))))
        x = self.pool(F.relu(self.bn2(self.conv2(x))))

        # Flatten
        x = x.view(-1, self.flattened_size)

        # FC layers
        x = self.fc(x)

        # Theta parameter prediction
        theta = self.fc_loc_final(x)
        theta = theta.view(-1, 2, 3)

        return theta


class STN_CNN(nn.Module):
    """Classic CNN with an integrated Spatial Transformer module."""

    def __init__(self, localizer_hidden_dim=64, localizer_fc_layers=3):
        super().__init__()

        # Spatial Transformer module
        self.localizer = Localizer(
            hidden_dim=localizer_hidden_dim,
            num_fc_layers=localizer_fc_layers,
        )

        # Classic CNN for classification
        self.classifier_conv = nn.Sequential(
            nn.Conv2d(1, 10, kernel_size=5),
            nn.MaxPool2d(2),
            nn.ReLU(),
            nn.Conv2d(10, 20, kernel_size=5),
            nn.Dropout2d(),
            nn.MaxPool2d(),
            nn.ReLU(),
        )

        # STN output 28x28 -> conv(24) -> pool(12) -> conv(8) -> pool(4)
        classif_size = 20 * 4 * 4
        self.classifier_fc = nn.Sequential(
            nn.Linear(classif_size, 50),
            nn.ReLU(),
            nn.Dropout(),
            nn.Linear(50, 10),
        )

    def spatial_transform(self, x):
        """Applies spatial transformation to the input."""
        theta = self.localizer(x)
        grid = F.affine_grid(theta, x.size(), align_corners=False)
        x_transformed = F.grid_sample(x, grid, align_corners=False)
        return x_transformed, theta

    def forward(self, x):
        x_transformed, theta = self.spatial_transform(x)
        features = self.classifier_conv(x_transformed)
        features = features.view(features.size(0), -1)
        output = self.classifier_fc(features)
        return output, theta, x_transformed


def train_stn_on_mnist(
    epochs=20,
    batch_size=64,
    lr=0.01,
    hidden_dim=64,
    num_fc_layers=3,
):
    """Trains an STN on MNIST with perturbations."""

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"Device: {device}")

    # Transformations: perturbations to force STN learning
    train_transform = transforms.Compose([
        transforms.RandomAffine(
            degrees=30, translate=(0.2, 0.2), scale=(0.8, 1.2)
        ),
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,)),
    ])

    test_transform = transforms.Compose([
        transforms.RandomAffine(degrees=30, translate=(0.2, 0.2)),
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,)),
    ])

    train_dataset = datasets.MNIST(
        "./data", train=True, download=True, transform=train_transform
    )
    test_dataset = datasets.MNIST(
        "./data", train=False, transform=test_transform
    )

    train_loader = torch.utils.data.DataLoader(
        train_dataset, batch_size=batch_size, shuffle=True, num_workers=2
    )
    test_loader = torch.utils.data.DataLoader(
        test_dataset, batch_size=1000, shuffle=False, num_workers=2
    )

    model = STN_CNN(
        localizer_hidden_dim=hidden_dim,
        localizer_fc_layers=num_fc_layers,
    ).to(device)

    optimizer = optim.Adam(model.parameters(), lr=lr)
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
    criterion = nn.CrossEntropyLoss()

    for epoch in range(epochs):
        model.train()
        total_loss = 0
        correct = 0
        total = 0

        for batch_idx, (data, target) in enumerate(train_loader):
            data, target = data.to(device), target.to(device)

            optimizer.zero_grad()
            output, theta, x_transformed = model(data)
            loss = criterion(output, target)
            loss.backward()
            optimizer.step()

            total_loss += loss.item()
            pred = output.argmax(dim=1)
            correct += pred.eq(target).sum().item()
            total += target.size(0)

        scheduler.step()

        model.eval()
        test_correct = 0
        test_total = 0
        with torch.no_grad():
            for data, target in test_loader:
                data, target = data.to(device), target.to(device)
                output, theta, x_transformed = model(data)
                pred = output.argmax(dim=1)
                test_correct += pred.eq(target).sum().item()
                test_total += target.size(0)

        train_acc = 100.0 * correct / total
        test_acc = 100.0 * test_correct / test_total
        avg_loss = total_loss / len(train_loader)

        print(
            f"Epoch {epoch+1}/{epochs} | "
            f"Loss: {avg_loss:.4f} | "
            f"Train accuracy: {train_acc:.1f}% | "
            f"Test accuracy: {test_acc:.1f}%"
        )

    return model


def visualize_transformations(model, num_samples=4):
    """Visualizes the transformations applied by the STN."""

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    test_transform = transforms.Compose([
        transforms.RandomAffine(degrees=30, translate=(0.2, 0.2)),
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,)),
    ])
    test_dataset = datasets.MNIST(
        "./data", train=False, transform=test_transform
    )

    model.eval()
    fig, axes = plt.subplots(num_samples, 2, figsize=(6, 3 * num_samples))

    with torch.no_grad():
        for i in range(num_samples):
            img, label = test_dataset[i]
            img_tensor = img.unsqueeze(0).to(device)

            _, theta, transformed = model(img_tensor)

            ax1 = axes[i, 0]
            ax1.imshow(img.squeeze(), cmap="gray")
            ax1.set_title(f"Before transformation (label={label.item()})")
            ax1.axis("off")

            ax2 = axes[i, 1]
            ax2.imshow(transformed.squeeze().cpu().numpy(), cmap="gray")

            theta_matrix = theta.squeeze().cpu().numpy()
            mat_str = "\\n".join([
                f"[{theta_matrix[0,0]:+.3f}, {theta_matrix[0,1]:+.3f}, {theta_matrix[0,2]:+.3f}]",
                f"[{theta_matrix[1,0]:+.3f}, {theta_matrix[1,1]:+.3f}, {theta_matrix[1,2]:+.3f}]",
            ])
            ax2.set_title(f"After STN\\nθ =\\n{mat_str}")
            ax2.axis("off")

    plt.tight_layout()
    plt.savefig("stn_visualization.png", dpi=200)
    plt.show()
    print("Visualization saved: stn_visualization.png")


if __name__ == "__main__":
    model = train_stn_on_mnist(
        epochs=20,
        batch_size=64,
        lr=0.01,
        hidden_dim=64,
        num_fc_layers=3,
    )

    torch.save(model.state_dict(), "stn_mnist_model.pt")
    print("Model saved: stn_mnist_model.pt")

    visualize_transformations(model, num_samples=4)

This implementation uses F.affine_grid to create the coordinate grid and F.grid_sample for bilinear interpolation — the two key PyTorch functions for spatial sampling. The localization network combines convolutional layers for feature extraction and fully connected layers for transformation parameter regression. Initialization toward the identity matrix ensures the localization network starts by transforming the image without any alteration, which avoids the problem of a “broken” transformation at the start of training.

Hyperparameters

The choice of Spatial Transformer Network hyperparameters significantly influences its performance. Here are the most important parameters to consider:

  • hidden_dim (Localizer): The hidden dimension of the localization network. A larger value such as 128 or 256 increases the localizer’s approximation capacity but also increases the risk of overfitting. For simple tasks like MNIST, 64 is sufficient. For complex tasks with natural images, 128 to 256 is recommended.
  • num_fc_layers: The number of fully connected layers in the localizer. Three layers constitute a good starting point. More layers allow the localization network to learn more complex relationships between image features and transformation parameters, but increased depth can make training more unstable.
  • Placement of the STN in the network: An STN placed early in the network operates on raw pixels and learns easily interpretable geometric transformations (rotation, translation, scaling). An STN placed later, after several convolutional layers, operates on higher-dimensional feature maps and can learn more abstract transformations, but they are less visually interpretable.
  • Sampling grid size: By default, the grid has the same dimension as the input image. Modifying this size affects interpolation precision and computational cost.
  • STN learning rate: The Spatial Transformer Network can use a different learning rate than the classifier. A localization network with a lower learning rate learns transformations more slowly, which can stabilize training in early epochs.

Advantages

  • Full differentiability: The STN is trained via standard backpropagation, without requiring explicit supervision on transformations. It learns by itself which transformations to apply to optimize the final task.
  • Significant improvement in spatial robustness: A CNN with an STN generalizes much better to rotated, translated, or deformed images than the same CNN without an STN, often with a gain of several percentage points in accuracy.
  • Generality: The STN can learn non-rigid transformations (beyond affine) if given a richer deformation grid — this is referred to as a dense STN for local deformations that affect different regions of the image independently.
  • Modular component: The Spatial Transformer Network inserts as a simple module into any existing network. You can even place several at different locations in the same network to compose multi-scale transformations.
  • Interpretability: You can visualize the matrix $\theta$ produced by the localization network to understand what transformations the network has learned. It is a window into the model’s spatial reasoning, something rare in deep learning.
  • Computational efficiency: The grid_sample operation is highly optimized on GPU and adds negligible computational cost compared to the convolutional layers of the main classifier.

Limitations

  • Affine transformations only: The basic STN can only represent affine transformations (rotation, translation, scaling, shearing). For local non-rigid deformations, such as a face that smiles or a leaf that folds, you need to use dense STNs with a richer grid parameterization and more parameters to learn.
  • Task dependency: The localization network learns the transformations most useful for the current classification task. If the task does not benefit from particular cropping, the STN may not learn meaningful transformations, making its addition unnecessary for that particular case.
  • Risk of degenerate transformations: Without proper regularization or identity initialization, the localization network can learn transformations that crush the image to zero or project it off-screen, especially at the start of training. This is why initialization toward the identity matrix is strongly recommended.
  • No total invariance: The STN mitigates the effects of spatial transformations but does not guarantee perfect invariance. It improves the network’s robustness but does not replace a proper architecture or good data augmentation for tasks requiring absolute invariance to geometric transformations.

4 Practical Use Cases

1. OCR and Text Recognition

In optical character recognition, text images rarely arrive perfectly aligned: pages are slightly rotated, lines are curved, letters are deformed by photographic perspective. An STN integrated into a character recognition network learns to crop each word, straighten lines, and correct perspective distortions. This is particularly useful for reading license plates, digitizing handwritten documents, or recognizing characters in uncontrolled shooting conditions, such as those encountered in mobile scanning applications.

2. Object Detection and Localization

In a two-stage object detection pipeline, an STN can serve as an attention cropping module. After a first rough detection pass that identifies regions of interest, the network uses the STN to zoom into the detected region and produce a crop centered on the object. This approach allows processing each detected object at high resolution without needing to process the entire image at high resolution, which considerably reduces computational cost while improving classification accuracy of detected objects.

3. Medical Imaging

Alignment of medical images is a fundamental problem in clinical image analysis: an MRI of the same patient taken two days apart has displacements and deformations between the two acquisitions. Using Spatial Transformer Networks for non-rigid alignment allows automatic and precise recalibration of three-dimensional volumes. The STN learns to apply local deformations to align anatomical structures, facilitating longitudinal tracking of tumors, comparison before and after treatment, and computer-assisted segmentation for surgical planning.

4. Augmented Reality and Robot Vision

For augmented reality applications, it is essential to understand the geometry of the scene: where objects are, at what angle, at what distance from the camera. An STN can serve as a geometric normalization module to preprocess images before they are analyzed by other components of the system. In robot vision, an STN can help a robotic arm understand the position and orientation of objects to be grasped, by transforming the image to place the target object in a canonical reference frame that the motion planner can use to compute the optimal grasping trajectory.

See Also

About Salah YAHIAOUI

Salah YAHIAOUI publie sur The Pingouin des tutoriels Python et des décryptages sur l’intelligence artificielle. Les cas pratiques récents précisent leurs données, leurs tests et leurs limites. À propos, méthode éditoriale et contact.

View all posts by Salah YAHIAOUI →