Build the mental model
Early object detectors like R-CNN worked in two separate stages: first, a region-proposal step scans the image and suggests a few thousand candidate boxes that might contain an object, then a second network crops and classifies each candidate individually. This is accurate but slow, since the classification network has to run once per proposed region. YOLO ('You Only Look Once') and other single-shot detectors take a fundamentally different approach: skip proposals entirely, and instead divide the image into a fixed grid, then have the network predict, in one single forward pass, what's inside every grid cell simultaneously. Each cell is responsible for detecting objects whose center falls inside it — the network doesn't 'look' at regions sequentially; it produces every prediction in parallel from one shared feature map.
Concretely, a single-shot detector's output at each grid cell is a fixed-size vector, repeated once per 'anchor' (a predefined box shape, like a tall box for pedestrians or a wide box for cars). For each anchor, the network predicts an objectness score (is there an object here at all?) plus 4 box-coordinate offsets (adjustments to the anchor's default position and size to fit the actual object). So a grid cell with 3 anchors and 5 values each needs 15 output channels at that spatial location. This is why the detection head is just a small convolutional layer stacked on top of the backbone's feature map: a 1x1 (or 3x3) convolution that maps in_channels to num_anchors * num_outputs, producing one prediction bundle per grid cell in a single dense tensor — no cropping, no per-region network calls, which is exactly what makes single-shot detection fast enough for real-time use.
Connect it to a real scenario
The Tutorial Platform could use a single-shot detection head like this to automatically locate UI elements — buttons, code blocks, input fields — inside screenshots embedded in lesson content, generating bounding boxes that drive both auto-generated alt-text ('screenshot showing a green Submit button') and interactive click-to-zoom hotspots, without needing the much slower two-stage pipeline since screenshots need to be processed in bulk whenever an author uploads a new lesson.
Try the working example
import torch
import torch.nn as nn
class TinyDetectionHead(nn.Module):
def __init__(self, in_channels, num_anchors=3, num_outputs=5):
super().__init__()
self.backbone = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1), # 32x32 -> 16x16
nn.ReLU(),
nn.Conv2d(16, in_channels, kernel_size=3, stride=2, padding=1), # 16x16 -> 8x8
nn.ReLU(),
)
self.head = nn.Conv2d(in_channels, num_anchors * num_outputs, kernel_size=1)
self.num_anchors = num_anchors
self.num_outputs = num_outputs
def forward(self, x):
features = self.backbone(x) # (batch, in_channels, grid_h, grid_w)
raw = self.head(features) # (batch, num_anchors*num_outputs, grid_h, grid_w)
batch, _, grid_h, grid_w = raw.shape
return raw.view(batch, self.num_anchors, self.num_outputs, grid_h, grid_w)
torch.manual_seed(0)
model = TinyDetectionHead(in_channels=32)
images = torch.randn(4, 3, 32, 32) # fake batch of 4 RGB images
predictions = model(images)
print(predictions.shape)torch.Size([4, 3, 5, 8, 8]) — the backbone's two stride-2 convolutions shrink the 32x32 input down to an 8x8 feature grid, and the detection head produces, for every one of the 64 grid cells, 3 anchors x 5 values each (1 objectness score + 4 box coordinates), reshaped into the (batch, num_anchors, num_outputs, grid_h, grid_w) tensor.5-minute try-it
Change num_anchors from 3 to 5 and re-run the model, then verify by hand that the raw (pre-reshape) channel count from self.head equals num_anchors * num_outputs (25) and that the final printed shape's second dimension updates to 5 — this mirrors how real detectors trade off more anchor shapes per cell against a larger output tensor.
One important caution
Hard-coding the final .view() call's grid_h and grid_w instead of reading them from raw.shape breaks the model the moment you feed in a different input resolution than what you tested with.
Treating the raw objectness value as a probability without applying a sigmoid first will produce nonsensical scores outside [0, 1] since the head's final conv has no activation function of its own.
Wikipedia — You Only Look Once — Computer Vision