TAI BUI
Back to curriculum
BuildPython

Video Understanding — Temporal Modeling

A video is a sequence of images plus the physics connecting them. Every model handles that connection one of three ways: time as an extra axis (3D conv), a sequence to attend over (transformer), or a feature extracted once per frame and pooled (2D+pool).

TIME: ~45 minutesPREREQUISITES: Phase 4 Lesson 03 (CNNs), Phase 4 Lesson 04 (Image Classification)

Learning Objectives

  • Distinguish the three main video-modelling approaches (2D+pool, 3D conv, spatio-temporal transformer) and predict their cost and accuracy trade-offs
  • Implement frame sampling, temporal pooling, and a 2D+pool baseline classifier in PyTorch
  • Explain why I3D's "inflated" 3D kernels transfer well from ImageNet weights and what a factorised (2+1)D conv does differently
  • Read the standard action-recognition datasets and metrics: Kinetics-400/600, UCF101, Something-Something V2; top-1 accuracy at the clip and video level

The Problem

A 30-second video at 30 fps is 900 images. Naively, video classification is just image classification run 900 times, then some kind of aggregation on top. That works fine when the action is visible in almost every frame: sports, cooking, exercise videos. It fails badly when the action is defined by motion itself — freeze any single frame of "pushing something from left to right" and all you get is two still objects, no motion in sight.

The core question for every video architecture is when temporal structure gets modelled, and how. That answer drives everything downstream: compute cost, pretraining strategy, whether you can reuse ImageNet weights, what datasets the model can even learn from.

The 2D CNN backbones and ImageNet pretraining from earlier lessons carry over untouched. What's different here is entirely about time: how you sample it, how you model it, and how you pool it back down to a single prediction.

Warm-up0/3
1. You freeze a single frame from a video of someone pushing a box from left to right across a table. Looking at only that one frame, can you tell which direction the box is moving?Warm-up
A single video frame showing a table with one box on it, with the direction the box is moving left as a question mark.
The one frame the stem describes, drawn on its own.
2. You have a CNN already trained well on millions of labeled photos. For a new but related task, which starting point usually trains faster and reaches better accuracy: those pretrained weights, or weights initialised at random?Warm-up
Two starting points, a model already trained on millions of photos and a model with random initial weights, each pointing to the same new related task, with training speed and final accuracy both left as a question mark.
Setup only; which starting point trains faster or reaches better accuracy is not shown.
3. Across machine learning research generally, why do labs typically report results on a shared public benchmark, like ImageNet for images, instead of each grading their own model on a private test set?Warm-up
Three different labs' models each pointing up to the same shared ImageNet dataset box, with a question mark about what that shared dataset lets you do with the three results.
ImageNet is the example the stem itself names; the question mark covers the reasoning, not given here.

The Concept

The three architectural families

Mermaid diagram

2D + pool

Take a 2D CNN (ResNet, EfficientNet, ViT) and run it independently on every sampled frame. Average (or max-pool, or attention-pool) the per-frame embeddings, then feed the pooled vector to a classifier.

Pros:

  • ImageNet pretraining transfers directly.
  • Simplest to implement.
  • Cheap: T frames times single-image inference cost.

Cons:

  • Cannot model motion: an action is just an aggregate of appearances here.
  • Temporal pooling is order-invariant, so "open door" and "close door" look the same.

When to use: appearance-heavy tasks, transfer learning on small video datasets, initial baselines.

3D convolutions

Replace 2D (H, W) kernels with 3D (T, H, W) kernels, so the network convolves over both space and time. Early family: C3D, I3D, SlowFast.

I3D's trick: take a pretrained 2D ImageNet model and "inflate" each 2D kernel by copying it along a new time axis, so a 3x3 2D conv becomes a 3x3x3 3D conv. That hands the 3D model strong pretrained weights instead of forcing it to train from scratch.

Pros:

  • Directly models motion.
  • I3D inflation gives free transfer learning.

Cons:

  • T/8 more FLOPs than the 2D counterpart (for a temporal kernel of 3 stacked 3 times).
  • Temporal kernels are small; long-range motion needs a pyramid or dual-stream approach.

When to use: action recognition where motion is the signal (Something-Something V2, Kinetics with motion-heavy classes).

Spatio-temporal transformers

Tokenise the video into a grid of space-time patches and attend across all of them, meaning each patch weighs every other patch by learned similarity: TimeSformer, ViViT, Video Swin, and VideoMAE are all variations on this idea.

Attention patterns that matter:

  • Joint: one big attention over (t, h, w), quadratic in T*H*W and expensive.
  • Divided: two attentions per block, one over time and one over space, which scales roughly linearly.
  • Factorised: time attention alternates with space attention across blocks.

Pros:

  • SOTA accuracy on every major benchmark.
  • Transfers from image transformers (ViT) via patch inflation.
  • Supports long-context video via sparse attention.

Cons:

  • Compute-hungry.
  • Requires careful attention pattern choice or the runtime balloons.

When to use: large datasets, high-fidelity video understanding, multi-modal video+text tasks.

Frame sampling

A 10-second clip at 30 fps is 300 frames; feeding all 300 to any model is wasteful. Standard strategies:

  • Uniform sampling: pick T frames evenly across the clip. Default for 2D+pool.
  • Dense sampling: a random contiguous T-frame window, common for 3D convs because motion needs neighbouring frames.
  • Multi-clip: sample multiple T-frame windows from the same video, classify each, and average predictions at test time.

T is usually 8, 16, 32, or 64. Higher T means more temporal signal at more compute.

Evaluation

Two levels:

  • Clip-level accuracy: the model sees one T-frame clip and reports top-1 (or top-k) accuracy.
  • Video-level accuracy: average clip-level predictions across multiple clips per video, which runs higher and more stable.

Always report both. A model that scores 78% clip / 82% video is relying heavily on test-time averaging; one that scores 80% / 81% is more robust per clip.

Datasets you'll meet

  • Kinetics-400 / 600 / 700: the general-purpose action dataset, 400k clips, sourced from YouTube URLs (many now dead).
  • Something-Something V2: motion-defined actions ("moving X from left to right"), unsolvable by 2D+pool.
  • UCF-101 and HMDB-51: older, smaller, still commonly reported.
  • AVA: action localisation in space and time, harder than plain classification.
Check: The Concept0/24
1. What is a 2D + pool video classifier?Define
The T sampled frames of one clip feeding a design named 2D + pool, with what the design does to those frames left as a question mark, and one class prediction coming out.
Setup only; what happens between the frames and the prediction is not shown.
2. What does a 3D convolution do that a 2D convolution does not?Define
A clip of three five by five frame grids, with a three by three block of cells marked inside frame 1 for the 2D kernel and the cells a 3D kernel covers left as a question mark.
Setup only; which cells the 3D kernel covers is not shown.
3. What is I3D's inflation trick?Define
A pretrained 2D ImageNet model on the left and a 3D video model on the right, with the inflation step between them drawn as a dashed box holding a question mark.
Setup only; what inflation does to the weights is not shown.
4. In a video transformer, what does 'divided attention' mean?Define
One video transformer block drawn as space-time patch tokens, a dashed attention stage holding a question mark, an MLP, and the next block.
Setup only; what the block's attention stage contains is not shown.
5. What is a 'clip' in video modelling?Define
A row of frame ticks standing for a 300-frame video file, feeding a dashed box labelled clip that holds a question mark, which then feeds a video model.
Setup only; what a clip holds is not shown.
6. What is the difference between clip-level and video-level accuracy?Define
Two cards side by side, clip-level accuracy and video-level accuracy for the same model and test videos, with how each one is computed left as a question mark.
Setup only; neither card's recipe is filled in.
7. What is Kinetics in video understanding?Define
A table of the four video benchmarks the lesson names, with the Kinetics row highlighted and every what-it-is cell left as a question mark.
Setup only; what each benchmark is stays masked.
8. Why does a 2D+pool video model fail on the Something-Something V2 dataset?Explain
Two four-frame strips of the same object X: in the first the object sits further right in each frame, in the second further left, with a question mark over whether their pooled features differ.
Setup only, matching the stem's own left-to-right versus right-to-left framing.
9. What trade-off separates 2D+pool from a 3D convolutional network?Explain
Two cards, 2D + pool and 3D convolution, each with its cost and whether it can model motion left as a question mark.
Setup only; neither card's cost or capability is filled in.
10. In a spatio-temporal transformer, what is 'joint' attention?Explain
Two frames of a clip drawn as three by three token grids with one token highlighted, and which of the tokens a joint attention connects left as a question mark.
A small illustrative grid; a real clip holds far more tokens.
11. Why is divided attention usually chosen over joint attention for video transformers?Explain
Two cards, joint attention and divided attention, each with its cost as the clip's token count grows left as a question mark.
Setup only; neither card's cost trend is filled in.
12. What is the difference between uniform sampling and dense sampling for selecting frames from a clip?Explain
Two rows of 20 frame ticks: pattern A highlights frames spread evenly across the row, pattern B highlights five adjacent frames in the middle of the row.
The two sampling shapes only; neither row is named uniform or dense here.
13. Why is dense sampling the common choice for 3D convolutions?Explain
Three adjacent highlighted frame boxes standing for a 3D kernel's three-frame time span, above a row of 16 ticks with three widely spread frames highlighted, and the reason a contiguous window is preferred left as a question mark.
Setup only; why the spacing matters is not shown.
14. What does 'multi-clip' sampling do at test time that uniform or dense sampling alone does not?Explain
One video timeline with three separate highlighted windows, each feeding its own prediction box, with an operation over the three predictions left as a question mark.
Setup only; the operation combining the three clip predictions is not shown.
15. Why does inflating a pretrained 2D kernel give a 3D model a useful starting point?Explain
Two cards for two ways to set a 3D kernel's starting weights, inflated from a pretrained 2D kernel and initialised at random, with what each one holds before training starts left as a question mark.
Setup only; what either starting point carries is not shown.
16. Why should an evaluation report both clip-level and video-level accuracy?Explain
Two bars for the same model, clip-level accuracy marked X percent and video-level accuracy marked Y percent, with what reporting both buys a reader left as a question mark.
Symbolic only; no actual accuracy numbers are shown.
17. Why is AVA considered harder than a plain classification benchmark such as Kinetics?Explain
Two cards for the same clip: Kinetics asking which action is happening and returning one label, and AVA with what it asks for left as a question mark.
Setup only; what AVA asks for is not shown.
18. Kinetics-400's clips are sourced from YouTube URLs. What practical problem does that create for reproducing older published results today?Explain
A card for Kinetics-400 clips sourced from YouTube URLs, with the current status of those original source videos left as a question mark.
Source is stated in the lesson's Datasets section; current status is not shown.
19. Model A scores 78% clip and 82% video accuracy. Model B scores 80% clip and 81% video accuracy. Which conclusion is best supported?Analyze
Four bars: Model A at 78 percent clip and 82 percent video accuracy, Model B at 80 percent clip and 81 percent video accuracy.
The 78, 82, 80 and 81 percent figures are exactly as given in the stem.
20. A 2D+pool classifier trained on plenty of data keeps confusing 'open door' with 'close door'. Which property of the design explains this?Analyze
A door seen from above across four frames with its leaf swinging open, and the same door across four frames with its leaf swinging back, with a question mark over whether their pooled features differ.
Setup only, matching the stem's own open-door versus close-door framing.
21. A 3D ResNet trains badly. Its sampler uses range(0, num_frames, num_frames // T). What is the most likely cause and the best first step?Analyze
The sampler line the stem gives, with an illustrative num_frames of 97 and T of 10, its step worked out, and the indices it returns left as a question mark.
Illustrative numbers computed by this script to match the shape of the Author's Note's own bug; not a line shipped in main.py.
22. A team has a few thousand labelled cooking videos, one GPU, and needs a first working classifier. The actions are visible in most individual frames. Which design fits?Design
A card describing a few thousand labelled cooking videos, one GPU, and actions visible in most individual frames, above a table of the three architecture families with every when-to-use cell replaced with a question mark.
The dataset size, GPU budget and appearance-heavy framing are from the stem; the table's cells are masked.
23. You must classify motion-defined actions like 'pushing something from left to right', and you already have a strong pretrained 2D ResNet. Which approach fits best?Design
A card describing motion-defined actions and a strong already-pretrained 2D ResNet, above the architecture table with every when-to-use cell replaced with a question mark.
The task description and the existing pretrained backbone are from the stem; the table's cells are masked.
24. You are building a video transformer for long clips with a large T, and the runtime balloons with joint attention. Which change fits?Design
A card describing a video transformer for long clips with a large T whose runtime balloons under joint attention, above a table of the three attention patterns with every cost cell replaced with a question mark.
The long-clip, large-T framing and the ballooning runtime are from the stem; the table's cells are masked.

Build It

Step 1: Frame sampler

Uniform and dense samplers that work on a list of frames (or a video tensor).

python
import numpy as np
 
def sample_uniform(num_frames_total, T):
    if num_frames_total <= T:
        return list(range(num_frames_total)) + [num_frames_total - 1] * (T - num_frames_total)
    step = num_frames_total / T
    return [int(i * step) for i in range(T)]
 
 
def sample_dense(num_frames_total, T, rng=None):
    rng = rng or np.random.default_rng()
    if num_frames_total <= T:
        return list(range(num_frames_total)) + [num_frames_total - 1] * (T - num_frames_total)
    start = int(rng.integers(0, num_frames_total - T + 1))
    return list(range(start, start + T))

Both return T indices that you use to slice the video tensor.

Step 2: A 2D+pool baseline

Run a 2D ResNet-18 over every frame, average-pool features, classify.

python
import torch
import torch.nn as nn
from torchvision.models import resnet18, ResNet18_Weights
 
class FramePool(nn.Module):
    def __init__(self, num_classes=400, pretrained=True):
        super().__init__()
        weights = ResNet18_Weights.IMAGENET1K_V1 if pretrained else None
        backbone = resnet18(weights=weights)
        self.features = nn.Sequential(*(list(backbone.children())[:-1]))  # global avg pool kept
        self.head = nn.Linear(512, num_classes)
 
    def forward(self, x):
        # x: (N, T, 3, H, W)
        N, T = x.shape[:2]
        x = x.view(N * T, *x.shape[2:])
        feats = self.features(x).view(N, T, -1)
        pooled = feats.mean(dim=1)
        return self.head(pooled)
 
model = FramePool(num_classes=10)
x = torch.randn(2, 8, 3, 224, 224)
print(f"output: {model(x).shape}")
print(f"params: {sum(p.numel() for p in model.parameters()):,}")

Eleven million parameters, ImageNet pretrained, runs per-frame, averages, classifies. This baseline often lands within 5-10 points of proper 3D models on appearance-heavy tasks, sometimes ahead of them, because it's riding a stronger ImageNet backbone.

Step 3: An I3D-style inflated 3D conv

Turn a single 2D conv into a 3D conv by repeating weights along a new time axis.

python
def inflate_2d_to_3d(conv2d, time_kernel=3):
    out_c, in_c, kh, kw = conv2d.weight.shape
    weight_3d = conv2d.weight.data.unsqueeze(2)  # (out, in, 1, kh, kw)
    weight_3d = weight_3d.repeat(1, 1, time_kernel, 1, 1) / time_kernel
    conv3d = nn.Conv3d(in_c, out_c, kernel_size=(time_kernel, kh, kw),
                        padding=(time_kernel // 2, conv2d.padding[0], conv2d.padding[1]),
                        stride=(1, conv2d.stride[0], conv2d.stride[1]),
                        bias=False)
    conv3d.weight.data = weight_3d
    return conv3d
 
conv2d = nn.Conv2d(3, 64, kernel_size=3, padding=1, bias=False)
conv3d = inflate_2d_to_3d(conv2d, time_kernel=3)
print(f"2D weight shape:  {tuple(conv2d.weight.shape)}")
print(f"3D weight shape:  {tuple(conv3d.weight.shape)}")
x = torch.randn(1, 3, 8, 56, 56)
print(f"3D output shape:  {tuple(conv3d(x).shape)}")

The division by time_kernel keeps the activation magnitudes roughly constant — skip it and you'll break batch-norm statistics on the very first pass.

Step 4: Factorised (2+1)D conv

Split a 3D conv into a 2D (spatial) and a 1D (temporal) conv. Same receptive field, fewer parameters, better accuracy on some benchmarks.

python
class Conv2Plus1D(nn.Module):
    def __init__(self, in_c, out_c, kernel_size=3):
        super().__init__()
        mid_c = (in_c * out_c * kernel_size * kernel_size * kernel_size) \
                // (in_c * kernel_size * kernel_size + out_c * kernel_size)
        self.spatial = nn.Conv3d(in_c, mid_c, kernel_size=(1, kernel_size, kernel_size),
                                 padding=(0, kernel_size // 2, kernel_size // 2), bias=False)
        self.bn = nn.BatchNorm3d(mid_c)
        self.act = nn.ReLU(inplace=True)
        self.temporal = nn.Conv3d(mid_c, out_c, kernel_size=(kernel_size, 1, 1),
                                  padding=(kernel_size // 2, 0, 0), bias=False)
 
    def forward(self, x):
        return self.temporal(self.act(self.bn(self.spatial(x))))
 
c = Conv2Plus1D(3, 64)
x = torch.randn(1, 3, 8, 56, 56)
print(f"(2+1)D output: {tuple(c(x).shape)}")

A full R(2+1)D network is the same as a ResNet-18 with every 3x3 conv replaced by Conv2Plus1D.

Check: Build It0/10
1. A (2+1)D factorised convolution splits a 3D conv into which two operations?Define
A single Conv3d with kernel (3, 3, 3) on the left, an arrow labelled (2+1)D splits it into, and a dashed box on the right standing for two operations left as a question mark.
Setup only, with the lesson's default kernel_size=3; the two operations are not named.
2. The lesson's FramePool baseline, a plain 2D ResNet-18 with average pooling over frames, sometimes matches or beats a proper 3D model on appearance-heavy action datasets. Why?Explain
Two bars on an appearance-heavy action dataset: FramePool at 79 percent, just ahead of a proper 3D model at 78 percent.
Illustrative numbers matching the stem's own matches-or-beats case, not a measured run.
3. How does a (2+1)D convolution differ from a full 3D convolution with the same receptive field?Explain
A single full 3D conv box beside the lesson's (2+1)D box, with their parameter counts and whether there is a non-linearity between the two steps both left as a question mark.
Setup only; neither property is resolved here.
4. Using sample_uniform from the lesson, what does sample_uniform(100, 4) return?Apply
The lesson's sample_uniform called with num_frames_total 100 and T 4, with the step size and the returned list both left as a question mark.
Setup only, using the call's own real arguments.
5. Using sample_uniform from the lesson, what does sample_uniform(5, 8) return?Apply
The lesson's sample_uniform called with num_frames_total 5 and T 8, with the returned list left as a question mark.
Setup only, using the call's own real arguments.
6. For model = FramePool(num_classes=10) and x = torch.randn(2, 8, 3, 224, 224), what shape does model(x) return?Apply
The lesson's FramePool.forward on an input shaped (2, 8, 3, 224, 224): folding to a (N, T, 512) feature tensor, pooling over T, then the linear head, with the final output shape left as a question mark.
Setup only, from the lesson's own FramePool class and this call's real input shape.
7. In inflate_2d_to_3d with time_kernel=3, one entry of the 2D weight is 0.9. What value does each of its three time-slice copies hold in the 3D weight?Apply
The lesson's inflate_2d_to_3d repeat-then-divide line, with one 2D weight entry given as 0.9 and the value of each of its three time-slice copies left as a question mark.
Setup only, using the exact 0.9 value and time_kernel the stem gives.
8. Given conv3d = inflate_2d_to_3d(nn.Conv2d(3, 64, kernel_size=3, padding=1, bias=False), time_kernel=3) and x = torch.randn(1, 3, 8, 56, 56), what is tuple(conv3d(x).shape)?Apply
The real inflated 3D kernel and padding this call produces, with an input shaped (1, 3, 8, 56, 56) and the output shape left as a question mark.
Setup only, from the lesson's own inflate_2d_to_3d and this call's real arguments.
9. For Conv2Plus1D(3, 64) with the default kernel_size=3, what is the intermediate channel count mid_c?Apply
The lesson's mid_c formula substituted with in_c 3, out_c 64, and kernel_size 3, reducing to 5184 integer-divided by 219, with the result left as a question mark.
Setup only, using the exact in_c, out_c and kernel_size the stem gives.
10. You inflate pretrained 2D convs to 3D by repeating weights along time but forget to divide by the temporal kernel size. What goes wrong on the very first pass?Analyze
The lesson's inflate_2d_to_3d with its divide-by-time_kernel step removed, and what that breaks on the first forward pass left as a question mark.
Setup only, matching the stem's own missing-division variant.

Use It

Two libraries cover production video work:

  • torchvision.models.video: R(2+1)D, MViT, Swin3D with pretrained Kinetics weights, same API as the image models.
  • pytorchvideo (Meta): model zoo, data loaders for Kinetics, SSv2, and AVA, plus standard transforms.

For Vision-Language video models (video captioning, video QA), use transformers (VideoMAE, VideoLLaMA, InternVideo).

Ship It

This lesson produces:

  • outputs/prompt-video-architecture-picker.md: a prompt that picks 2D+pool / I3D / (2+1)D / transformer based on appearance-vs-motion, dataset size, and compute budget.
  • outputs/skill-frame-sampler-auditor.md: a skill that inspects a video pipeline's sampler and flags common bugs, like off-by-one indices, uneven sampling when num_frames < T, or a missing aspect-preserving crop.
Author's Note

The first video classifier I built went straight to a 3D ResNet, because it felt like the serious architecture for the job. Two days of debugging later, the bug wasn't in the model — it was in the frame sampler. I was slicing frames with something like range(0, num_frames, num_frames // T), which silently drops the tail of the clip whenever num_frames isn't a clean multiple of T. The model looked broken. The data feeding it was broken, and the model was faithfully learning from garbage.

If you take one habit from this lesson into production, take this: build the 2D+pool baseline first, even when you're sure the task needs motion. It's cheap, it reuses ImageNet weights, and it doubles as a sanity check on your data pipeline before you spend a week debugging a 3D conv that never had a chance because the frames going in were already wrong. Reach for I3D or a transformer once the baseline works and you've confirmed, on your own data, that appearance alone isn't enough.

Exercises

  1. (Easy) Compute FLOPs (approximate) for FramePool with T=8 vs an I3D-style 3D ResNet with T=8. Justify why 2D+pool is 3-5x cheaper.
  2. (Medium) Generate a synthetic video dataset: random balls moving in random directions, labelled by direction of motion ("left-to-right", "right-to-left", "diagonal-up"). Train FramePool on it. Show that it achieves near-chance accuracy, proving appearance alone is insufficient for motion tasks.
  3. (Hard) Build an R(2+1)D-18 by replacing every Conv2d in a ResNet-18 with Conv2Plus1D. Inflate the first conv's weights from an ImageNet-pretrained ResNet-18. Train on the motion dataset from exercise 2 and beat FramePool.

Key Terms

TermWhat people sayWhat it actually means
2D + pool"Per-frame classifier"Run a 2D CNN on every sampled frame, average-pool features across time, classify
3D convolution"Spatio-temporal kernel"Kernel that convolves over (T, H, W); can model motion natively
Inflation"Lift 2D weights to 3D"Initialise 3D conv weights by repeating a 2D conv's weights along the new time axis, then divide by kernel_T to preserve activation scale
(2+1)D"Factorised conv"Split 3D into 2D spatial + 1D temporal; fewer parameters, extra non-linearity between
Divided attention"Time then space"Transformer block with two attentions per layer: one over tokens at the same frame, one over tokens at the same position
Clip"T-frame window"A sampled subsequence of T frames; the unit a video model consumes
Clip vs video accuracy"Two eval settings"Clip = one sample per video, video = average across multiple sampled clips
Kinetics"The ImageNet of video"400-700 action classes, 300k+ YouTube clips, the standard video pretraining corpus

Further Reading