A pure-Transformer video backbone that beats factorized space-time attention by doing the opposite of "attend to everything": it confines attention to small 3D windows and shifts them between layers. The locality inductive bias from 2D Swin, lifted into time.
You want to recognize an action in a short clip: someone "pushing something from left to right." Your clip is 32 frames of 224×224 RGB. The obvious move in 2021 was: take the Vision Transformer that just conquered images, cut the video into little space-time cubes, flatten them into a sequence of tokens, and run plain global self-attention over the whole thing.
Let's count the tokens. A ViT splits each frame into 16×16 patches, giving 14×14 = 196 patches per frame. Over 32 frames that is 196 × 32 = 6272 tokens. Global self-attention compares every token with every other token, so it builds an attention matrix of size 6272 × 6272 ≈ 39 million entries — per head, per layer. Self-attention is O(T2) in the number of tokens, and in video the token count explodes because you multiplied the spatial grid by the temporal length.
Slide the number of frames. Global attention cost grows with the square of total tokens (purple). 3D window attention (teal) keeps cost linear in the clip size, because each window only attends within itself. Watch the gap open.
The early video transformers (ViViT, TimeSformer) survived this by factorizing attention: do spatial attention within a frame, then temporal attention across frames, in separate steps. That trims the cost, but it hand-designs how space and time interact and it still leans on global attention inside each factor. The 3D structure of motion — a hand moving diagonally through space and time together — gets split apart.
The Video Swin Transformer (Liu et al., CVPR 2022) asks a different question. Instead of "how do we make global attention over 6272 tokens affordable?", it asks: does a token in the top-left of frame 0 really need to attend directly to a token in the bottom-right of frame 31? In most video, the answer is no. Nearby pixels in space and time are what carry an action. So confine attention to small 3D windows of neighboring space-time tokens, and recover long-range reach by shifting the windows between layers.
| Approach | Attention scope | Cost in tokens N | Space-time coupling |
|---|---|---|---|
| Global (ViViT joint) | all N tokens | O(N2) | Full, but unaffordable |
| Factorized (TimeSformer) | spatial then temporal | O(N·(HW + T)) | Hand-split into 2 axes |
| Video Swin (3D window) | tokens inside a P×M×M window | O(N) — linear | Joint, local, then shifted |
Video Swin's headline result: with this design it set new state-of-the-art on Kinetics-400, Kinetics-600 and Something-Something V2 at the time, while using a smaller model and roughly an order of magnitude less pre-training data than the global-attention ViViT it beat. The whole architecture is the 2D Swin Transformer with one change — every 2D window becomes a 3D window. That is the entire paper, and the rest of this lesson is unpacking why that one change is so powerful.
An inductive bias is an assumption you build into a model's architecture that constrains what it can express, so it can learn the right answer from less data. A convolution's inductive bias is locality and translation equivariance: a feature detector slides over the image and only looks at a small neighborhood. That assumption is why CNNs are so data-efficient on images — you don't waste capacity learning that distant, unrelated pixels matter.
Global self-attention deliberately throws that bias away. Every token can attend to every other token, so the model has to learn from scratch which connections matter. On enormous datasets (JFT-300M) ViT learns this fine. On the smaller video datasets available in 2021 it overfits, because the hypothesis space is too big.
Concretely, Video Swin keeps the 2D Swin recipe and extends the locality prior to the time axis. The two ingredients of Swin are:
To see why locality + shifting recovers global reach, picture a token's receptive field. In layer 1 it sees its own window. After the shift in layer 2, the window has moved, so it now overlaps tokens from a different layer-1 window — it can pull in information that crossed the old boundary. Each window+shift pair roughly doubles the reach. This is exactly the convolutional intuition that stacking 3×3 convolutions builds a large receptive field, applied to attention windows.
A 1D row of tokens. The center token (warm) is the query. Step through layers: with windowing+shift, its receptive field (teal) grows roughly one window per pair of layers — reaching the whole row after a few layers, at linear cost. Global attention reaches everything in one layer but at quadratic cost.
Suppose a spatial axis has 56 tokens and the window is M = 7 wide. A window covers 7 tokens. After the first window layer the query reaches 7 tokens. The shift of M/2 ≈ 3 lets the next layer's window overlap a neighbor, extending reach by about one window (7) every two layers. To cover 56 tokens you need roughly 56/7 ≈ 8 windows of growth, so about 2 × 8 = 16 layers — comfortably inside the depth of stage 3 (which has many transformer blocks). The point: global reach is reached by depth, not by paying O(N2) in any single layer.
Before any attention happens, we have to turn a video tensor into tokens. The input clip is a tensor of shape [T, H, W, 3] — T frames, height H, width W, 3 colour channels. In the paper's default setup T = 32, H = W = 224.
Video Swin's 3D patch partition cuts the clip into non-overlapping 3D patches of size 2×4×4×3 — 2 frames deep, 4×4 in space, all 3 channels. Each such cube of 2·4·4·3 = 96 raw values becomes one token. A linear embedding then projects those 96 numbers to the model dimension C (96 for the Tiny model).
Let's trace the shapes for the default clip. T/2 = 16 temporal tokens, H/4 = 56 spatial rows, W/4 = 56 spatial columns. So after patch partition we have a token grid of 16 × 56 × 56, each token a C-dimensional vector. That is 16·56·56 = 50,176 tokens — a lot, which is exactly why we will never run global attention over all of them.
A small clip is diced into 2×4×4 space-time cubes. Each cube (highlighted) is flattened and linearly projected into one C-dim token. Slide to walk the partition cube through the clip; watch the source pixels it gathers.
The temporal patch size of 2 is a deliberate choice. A patch size of 1 would make each token belong to exactly one frame, deferring all temporal mixing to attention. Bundling 2 adjacent frames into each token means motion between those two frames is captured at the very first layer, by the linear embedding itself. It also halves the temporal token count (32 frames → 16 temporal tokens), cutting cost. The paper uses a small temporal patch so that fine motion is not blurred away.
Swin is hierarchical: it starts with a fine token grid and progressively merges patches to build a feature pyramid, like a CNN. A small initial patch (4×4) gives a high-resolution stage 1 (56×56), which downsampling later coarsens to 28×28, 14×14, 7×7. ViT, by contrast, uses one big 16×16 patch and a single resolution throughout. The hierarchy is what makes Swin usable as a general backbone for detection and segmentation, and Video Swin inherits it.
python import torch import torch.nn as nn # 3D patch partition = strided Conv3d (kernel == stride) patch_embed = nn.Conv3d(3, 96, kernel_size=(2,4,4), stride=(2,4,4)) clip = torch.randn(1, 3, 32, 224, 224) # [B, 3, T, H, W] tokens = patch_embed(clip) # [1, 96, 16, 56, 56] tokens = tokens.permute(0,2,3,4,1) # [1, 16, 56, 56, 96] (B, T', H', W', C) # 16*56*56 = 50176 tokens — far too many for global attention
Now the central mechanism. We have a token grid of shape [T', H', W', C]. 3D window-based multi-head self-attention (3D W-MSA) partitions this grid into non-overlapping 3D windows of size P×M×M — default 2×7×7 — and runs ordinary multi-head self-attention independently inside each window.
Each window therefore contains P·M·M = 2·7·7 = 98 tokens. Self-attention inside a window builds a 98×98 attention matrix — tiny and fixed, regardless of how big the clip is. The number of windows scales with the clip size, but the cost per window is constant, so the total cost is linear in the number of tokens.
Let h = T', and the spatial grid be h×w tokens (using w loosely for both spatial dims). Global MSA over N = T'·H'·W' tokens costs about 4NC2 + 2N2C — the second term, 2N2C, is the quadratic attention that kills us. Window MSA over windows of P·M·M tokens costs about 4NC2 + 2(P·M·M)NC. The attention term is now 2(P·M·M)NC: linear in N, because the window size PMM is a small constant.
For N = 50,176 and window 2·7·7 = 98, the attention term shrinks from 2N2C to 2·98·NC — a reduction factor of N/98 ≈ 512×. That is the whole ballgame.
Inside a window, plain self-attention is permutation-invariant — it does not know where within the window each token sits. Video Swin adds a learned 3D relative position bias B to the attention logits, one per pair of relative offsets along (time, height, width):
Because a P×M×M window has relative offsets ranging over (2P−1)×(2M−1)×(2M−1) possibilities, B is read from a small learned table of that size. For 2×7×7 the table is 3×13×13 = 507 entries per head — cheap, and it encodes the within-window geometry the attention scores otherwise ignore.
A token grid (here 4 temporal slices, each an 8×8 spatial grid) is cut into 2×4×4 windows (smaller than the default, for legibility). Each colored block is one window; attention runs only inside it. Slide to inspect one window's tokens — the 98-token default becomes one of these blocks.
python def window_partition_3d(x, P, M): """x: [B, T, H, W, C] -> windows: [num_windows*B, P*M*M, C]""" B, T, H, W, C = x.shape # group each axis into (#windows_along_axis, window_extent) x = x.view(B, T//P, P, H//M, M, W//M, M, C) # bring the three window-extent axes together, then flatten windows = x.permute(0,1,3,5,2,4,6,7).reshape(-1, P*M*M, C) return windows # attention runs over the P*M*M axis, per window # default: P=2, M=7 -> each window has 2*7*7 = 98 tokens # a 16x56x56 grid yields (16/2)*(56/7)*(56/7) = 8*8*8 = 512 windows
Window attention has a problem you can probably already see: tokens on opposite sides of a window boundary never attend to each other. The action you care about — a hand crossing from one window into the next — would be split by a wall the model cannot see across. If windows never moved, the receptive field would be permanently capped at one window, no matter how deep the network.
Swin's fix, lifted to 3D: alternate between two layer types. Even layers use 3D W-MSA (regular windows). Odd layers use 3D SW-MSA — shifted window MSA — where the window grid is displaced by half a window along each axis before partitioning. The default shift is (P/2, M/2, M/2) = (1, 3, 3) for the 2×7×7 window.
Because the shifted grid is offset by half a window, the new windows straddle the boundaries of the previous (regular) windows. A token that was on the left edge of one regular window is now in the middle of a shifted window, sitting next to tokens that used to be in the neighboring regular window. So in the shifted layer, those previously-separated tokens finally attend to each other. Cross-window information flows.
Top: regular windows partition the tokens (the boundaries, in red, block information). Bottom: shifted windows are offset by M/2, so each shifted window spans two regular windows — tokens that were on opposite sides of a red boundary now share a window. Toggle to compare.
Shifting creates a headache. After offsetting the grid by half a window, the windows at the edges of the volume are no longer full P×M×M cubes — they are partial. A naive implementation would now have to handle many differently-sized windows, which is slow and ugly. For the 2×7×7 / shift (1,3,3) case, a straightforward shift turns each axis's window count from k into k+1, with the extra windows being fragments.
Take a 1D axis of 8 tokens [0..7], window M = 4, shift = M/2 = 2. Regular windows: {0,1,2,3} and {4,5,6,7}. The boundary sits between token 3 and token 4 — they never meet. After a cyclic roll by −2 (move everything 2 to the left, wrapping), the sequence becomes [2,3,4,5,6,7,0,1]. Now window 1 = {2,3,4,5}: tokens 3 and 4, formerly split, are together. Window 2 = {6,7,0,1}: this one mixes the true tail (6,7) with the wrapped head (0,1), which are not neighbors — that is precisely where the mask will block attention.
This is the most intricate engineering in the architecture, and it is the second Code Lab's target. The shifted-window layer is implemented in three steps: cyclic shift → window partition → masked attention → reverse cyclic shift.
We roll the token volume by the negative shift along each axis. In code this is torch.roll(x, shifts=(−P/2, −M/2, −M/2), dims=(1,2,3)). Tokens that fall off one edge wrap around to the other. After this roll, partitioning with the regular (unshifted) grid is equivalent to having used shifted windows on the original volume — and crucially, every window is full-sized. No padding, no ragged edges, no change in window count.
The roll glues together tokens from opposite ends of the volume. Inside a window that sits on the wrap-around seam, you now have two groups of tokens that are not spatial-temporal neighbors — they only ended up together because of the roll. If we let them attend freely, the model would hallucinate a connection between, say, the bottom of the clip and the top. We must forbid attention across the seam while still allowing attention within each genuine group.
Each token in a seam window gets a group id — which original region it came from. Two tokens may attend only if they share a group id. We build an attention bias matrix where allowed pairs get 0 and forbidden pairs get a large negative number (−100, effectively −∞). Adding this to the attention logits before softmax drives the forbidden weights to zero:
The group ids come from slicing each axis into three regions: the bulk (size M−shift), the to-be-wrapped slice (size shift), and the boundary, then labeling cells by which (time, height, width) region they belong to. Because the mask depends only on the geometry, it is precomputed once and reused for every clip.
A 1D ring of 8 tokens, window M=4, shift=2. Step through: (1) roll by −2, (2) partition into windows, (3) see the seam window mixing two groups, (4) view the mask that blocks cross-group attention (red = forbidden). The genuine windows stay fully connected.
After masked attention, we undo the roll with the opposite shift so tokens return to their true positions before the next layer. The whole shifted-window mechanism is thus transparent to the rest of the network — it takes a token grid in, gives a token grid out, same shape, with cross-window information mixed in.
python def shifted_window_attention(x, P, M, shift, attn, mask): """x: [B, T, H, W, C]. shift = (P//2, M//2, M//2).""" # 1) cyclic shift: roll so shifted windows become regular full windows x = torch.roll(x, shifts=(-shift[0], -shift[1], -shift[2]), dims=(1,2,3)) # 2) partition into P*M*M windows (now all full-sized) wins = window_partition_3d(x, P, M) # [nW*B, P*M*M, C] # 3) attention WITH the precomputed mask (-inf across the seam) out = attn(wins, mask=mask) # forbidden pairs -> 0 weight # 4) un-partition and REVERSE the cyclic shift x = window_reverse_3d(out, P, M, x.shape) x = torch.roll(x, shifts=(shift[0], shift[1], shift[2]), dims=(1,2,3)) return x
We now have the block: a regular-window layer followed by a shifted-window layer, each wrapped in LayerNorm, residual connections, and a 2-layer MLP. The full Video Swin backbone is a hierarchy of four stages, each a stack of these blocks, with patch merging for spatial downsampling between stages — exactly mirroring 2D Swin.
Patch merging downsamples the spatial grid by 2× between stages (it does not touch the temporal axis). It groups each 2×2 neighborhood of spatial tokens, concatenates their features into a 4C vector, then linearly projects to 2C. So height and width halve while channels double — the standard CNN feature-pyramid trade. Temporal resolution is left alone because video clips are usually short in time and you do not want to collapse motion too aggressively.
| Stage | Temporal × Spatial grid | Channels (Swin-T) | Operation |
|---|---|---|---|
| Patch partition | 16 × 56 × 56 | C = 96 | 3D conv, 2×4×4 |
| Stage 1 | 16 × 56 × 56 | 96 | 2 blocks |
| Stage 2 | 16 × 28 × 28 | 192 | patch merge + 2 blocks |
| Stage 3 | 16 × 14 × 14 | 384 | patch merge + 6 blocks |
| Stage 4 | 16 × 7 × 7 | 768 | patch merge + 2 blocks |
Note the temporal dimension stays at 16 throughout — only space is pyramided. The final stage outputs a 16×7×7 grid of 768-dim tokens. Global average pooling over all those tokens, then a linear classifier, produces the action label.
Walk through the backbone. Each stage downsamples space by 2× (patch merge) and doubles channels, while the temporal axis stays fixed. Click "Next Stage" to advance; the highlighted block shows the current grid shape and channel count.
Video Swin comes in T/S/B/L sizes that mirror 2D Swin's depths and widths. The masterstroke is initialization by inflation: a Video Swin is initialized from a 2D Swin pre-trained on ImageNet. The 2D weights transfer directly except for two places. The patch-embedding conv gains a temporal dimension (the 2D filter is replicated and divided by the temporal patch size, so its mean output matches), and the relative-position-bias table is replicated across the new temporal offsets. This lets Video Swin inherit strong image features and reach state-of-the-art with far less video data than a from-scratch global model.
W3d[t] = W2d / P for each of the P temporal positions. Replicating and dividing by P means a static (unchanging) clip produces the same activation as the 2D model on a single frame — the temporal extension starts as a no-op and learns motion from there. That careful normalization is why inflation transfers cleanly instead of blowing up the activation scale.The claim Video Swin has to back up: locality beats globality for video, given realistic data. The headline numbers (at publication) on the three standard benchmarks:
| Benchmark | What it tests | Video Swin (best) | Significance |
|---|---|---|---|
| Kinetics-400 | appearance-heavy actions | ~84.9% top-1 | State-of-the-art at publication, beating ViViT |
| Kinetics-600 | larger action vocabulary | ~85.9% top-1 | New SOTA |
| Something-Something V2 | temporal reasoning (motion-heavy) | ~69.6% top-1 | New SOTA on the hardest temporal benchmark |
The two most important framings of these results:
The single most load-bearing ablation removes the shifted-window mechanism — use only regular 3D windows, never shifted. Accuracy drops, because without the shift, information can never cross window boundaries and the receptive field stays capped at one window. This directly confirms the Chapter 4 story: windowing alone is local; the shift is what makes the network global.
Bars show the qualitative accuracy impact of removing each component, relative to the full model. Removing the 3D shift and using factorized (separate space/time) attention both hurt most; these reproduce the paper's reported trends. Click a bar to read why.
Two more design knobs were studied. First, the window size: 2×7×7 was a sweet spot — too small a window starves each layer of context; too large a window erodes the efficiency advantage and the locality prior. Second, the temporal dimension of the window matters: a window that spans multiple frames (P = 2) lets joint space-time attention happen inside the window, which is exactly what distinguishes Video Swin from a per-frame 2D Swin. The 3D joint window outperformed treating time separately, validating the joint (non-factorized) design.
This is the payoff. Everything from Chapters 3–5 in one interactive view of a token grid. Pick a query token; choose regular or shifted windows; set the window size and shift. The simulation partitions the grid into 3D windows, shows which tokens the query can attend to, applies the cyclic shift and seam mask when shifted, and reports the cost. Break it: shrink the window and watch the receptive field starve; turn off the shift and watch the query get boxed in by boundaries.
Two temporal slices of a token grid. The warm token is the query; teal tokens are what it attends to inside its window. Toggle the shift to see windows straddle boundaries and the query's reach jump. Adjust window size M and watch the linear-cost readout update. Click any token to move the query.
Things to try, each of which maps to a claim earlier in the lesson:
Video Swin is a beautifully economical idea, but it is not the last word. Its assumptions and costs are worth naming clearly.
| Limitation | Why it bites |
|---|---|
| Fixed window geometry | P×M×M is hand-set. Actions span very different space-time scales; a single window size cannot be optimal for both a flick of a finger and a sweeping camera pan. |
| Short temporal coverage | The temporal axis is never downsampled and clips are short (32 frames). Long-horizon actions that unfold over minutes are out of reach for one forward pass. |
| Locality prior can mislead | For genuinely long-range temporal dependencies (cause now, effect much later), needing many layers to connect distant frames is less direct than true global attention. |
| Shift/mask complexity | The cyclic-shift + masking machinery is correct but fiddly; bugs in the mask cause silent, hard-to-detect accuracy loss. |
| Quantity | Default | Meaning |
|---|---|---|
| Patch size | 2×4×4 | 3D patch partition (time×H×W); 1 token per cube |
| Window size P×M×M | 2×7×7 = 98 tokens | Scope of one attention op |
| Shift | (1, 3, 3) = (P/2, M/2, M/2) | Offset for SW-MSA layers |
| Stages | 4 (depths 2,2,6,2 for T) | Each = W-MSA then SW-MSA blocks |
| Patch merge | 2× spatial down, 2× channels | Temporal axis untouched |
| Attention cost | O(N) [4NC² + 2(PMM)NC] | Linear, vs O(N²) global |
| Rel. pos. bias | (2P−1)(2M−1)² per head | 3D within-window geometry, learned |
| Init | Inflated 2D Swin (W3d=W2d/P) | Image features transfer to video |
Build out the surrounding map of ideas:
"What I cannot create, I do not understand." — and you have now built the 3D window partition and the seam mask by hand, which is the whole engine.