비디오 이해 — 시간적 모델링(Temporal Modeling)
비디오는 이미지의 시퀀스(sequence)와 그 이미지들을 이어 주는 물리 법칙입니다. 모든 비디오 모델(video model)은 시간을 추가 축으로 다루거나(3D conv), 어텐션(attention)을 적용할 시퀀스로 다루거나, 한 번 특징(feature)을 뽑은 뒤 풀링(pooling)할 대상으로 다룹니다(2D+pool).
유형: Learn + Build
언어: Python
선수 학습: Phase 4 Lesson 03 (CNN), Phase 4 Lesson 04 (Image Classification)
소요 시간: 약 45분
학습 목표
- 2D+pool, 3D 합성곱(3D convolution), 시공간 트랜스포머(spatio-temporal transformer)라는 세 가지 주요 비디오 모델링 접근(video modeling approach)을 구분하고 비용과 정확도(accuracy)의 트레이드오프(trade-off)를 예측합니다.
- PyTorch로 프레임 샘플링(frame sampling), 시간 풀링(temporal pooling), 2D+pool 베이스라인 분류기(baseline classifier)를 구현합니다.
- I3D의 인플레이션된(inflated) 3D 커널(kernel)이 ImageNet 가중치(weight)에서 잘 전이(transfer)되는 이유와 인수분해된(factorised) (2+1)D 합성곱(conv)이 무엇을 다르게 하는지 설명합니다.
- Kinetics-400/600, UCF101, Something-Something V2 같은 표준 행동 인식(action-recognition) 데이터셋(dataset)과 클립(clip) 단위·비디오 단위 top-1 정확도를 읽습니다.
문제
30 fps의 30초짜리 비디오는 900장의 이미지입니다. 단순하게 보면 비디오 분류(video classification)는 이미지 분류(image classification)를 900번 수행한 뒤 어떤 방식으로든 집계(aggregation)하는 문제입니다. 이 방식은 행동이 거의 모든 프레임(frame)에서 보이는 경우, 예를 들어 스포츠, 요리, 운동 영상에서는 어느 정도 작동합니다. 하지만 행동이 동작(motion) 자체로 정의되는 경우, 예를 들어 "무언가를 왼쪽에서 오른쪽으로 밀기" 같은 동작은 어떤 단일 프레임(single frame)을 보더라도 두 정지 물체처럼 보이므로 크게 실패합니다.
모든 비디오 아키텍처(video architecture)의 핵심 질문은 시간적 구조(temporal structure)를 언제, 어떻게 모델링하는가입니다. 이 답이 연산 비용(compute cost), 사전학습 전략(pretraining strategy), ImageNet 가중치를 재사용할 수 있는지 여부, 학습 데이터셋 선택까지 모두 결정합니다.
이 레슨은 정적 이미지 레슨보다 짧습니다. 핵심 이미지 처리 장치는 이미 배웠고, 비디오 이해(video understanding)는 대부분 샘플링(sampling), 모델링(modeling), 집계(aggregating)라는 시간적 흐름에 관한 문제입니다.
개념
세 가지 아키텍처 가족
flowchart LR
V["Video clip<br/>(T frames)"] --> A1["2D + pool<br/>run 2D CNN per frame,<br/>average over time"]
V --> A2["3D conv<br/>convolve over<br/>T x H x W"]
V --> A3["Spatio-temporal<br/>transformer<br/>attention over<br/>(t, h, w) tokens"]
A1 --> C["Logits"]
A2 --> C
A3 --> C
style A1 fill:#dbeafe,stroke:#2563eb
style A2 fill:#fef3c7,stroke:#d97706
style A3 fill:#dcfce7,stroke:#16a34a
2D + pool
2D CNN(ResNet, EfficientNet, ViT)을 가져와 샘플링된 프레임마다 독립적으로 실행합니다. 프레임별 임베딩(embedding)을 평균(average), 맥스 풀링(max-pool), 어텐션 풀링(attention-pool) 중 하나로 합칩니다. 합쳐진 벡터를 분류기(classifier)에 넣습니다.
장점:
- ImageNet 사전학습(pretraining)을 그대로 전이(transfer)할 수 있습니다.
- 구현이 가장 단순합니다.
- 비용이 저렴합니다. 비용은 T개 프레임 × 이미지 한 장당 추론 비용(inference cost)입니다.
단점:
- 동작을 모델링하지 못합니다. 행동은 외형(appearance)의 총합으로 환원됩니다.
- 시간 풀링은 순서에 무관(order-invariant)합니다. 그래서 "문 열기"와 "문 닫기"가 똑같아 보입니다.
사용할 때: 외형이 중요한 과제, 작은 비디오 데이터셋에서의 전이 학습(transfer learning), 초기 베이스라인(baseline).
3D 합성곱(3D convolutions)
2D (H, W) 커널을 3D (T, H, W) 커널로 바꿉니다. 네트워크는 공간(space)과 시간(time)을 함께 합성곱(convolution)합니다. 초기 계열에는 C3D, I3D, SlowFast가 있습니다.
I3D 기법(I3D trick)은 사전학습된 2D ImageNet 모델을 가져와 각 2D 커널을 새로운 시간 축(time axis)을 따라 복사해 "부풀리는(inflate)" 것입니다. 3x3 2D 합성곱은 3x3x3 3D 합성곱이 됩니다. 이렇게 하면 3D 모델을 처음부터 학습하지 않고도 강력한 사전학습 가중치를 얻을 수 있습니다.
장점:
- 동작을 직접 모델링합니다.
- I3D 인플레이션(inflation)으로 전이 학습을 거의 공짜로 얻을 수 있습니다.
단점:
- 2D 모델 대비 FLOPs가 훨씬 큽니다.
- 시간 커널이 작기 때문에 장거리 동작(long-range motion)에는 피라미드(pyramid)나 듀얼 스트림(dual-stream) 접근이 필요합니다.
사용할 때: Something-Something V2나 동작이 많은 Kinetics 클래스(class)처럼 동작이 신호(signal)가 되는 행동 인식.
비디오를 시공간 패치 격자(space-time patch grid)로 토큰화(tokenise)하고 전체 토큰에 어텐션을 적용합니다. TimeSformer, ViViT, Video Swin, VideoMAE가 이 계열입니다.
중요한 어텐션 패턴(attention pattern):
- 결합(Joint) —
(t, h, w) 전체에 한 번의 큰 어텐션을 적용합니다. T*H*W에 대해 제곱(quadratic)으로 비례하므로 비쌉니다.
- 분할(Divided) — 블록마다 두 개의 어텐션을 둡니다. 하나는 시간 방향에, 하나는 공간 방향에 적용합니다. 규모 확장이 훨씬 수월합니다.
- 인수분해(Factorised) — 블록마다 시간 어텐션과 공간 어텐션을 교대로 적용합니다.
장점:
- 주요 벤치마크(benchmark)에서 SOTA 정확도를 냅니다.
- 패치 인플레이션(patch inflation)을 통해 이미지 트랜스포머(image transformer)인 ViT에서 전이할 수 있습니다.
- 희소 어텐션(sparse attention)으로 긴 맥락(long-context) 비디오를 지원합니다.
단점:
- 연산량이 큽니다.
- 어텐션 패턴을 신중하게 고르지 않으면 실행 시간(runtime)이 크게 늘어납니다.
사용할 때: 큰 데이터셋, 고품질 비디오 이해, 비디오와 텍스트를 함께 다루는 다중 모달(multi-modal) 과제.
프레임 샘플링(Frame sampling)
30 fps에서 10초짜리 클립은 300프레임입니다. 300장을 모두 모델에 넣는 것은 낭비입니다. 표준 전략은 다음과 같습니다.
- 균일 샘플링(Uniform sampling) — 클립 전체에서 T개의 프레임을 고르게 골라냅니다. 2D+pool의 기본값입니다.
- 밀집 샘플링(Dense sampling) — T개의 연속된 프레임 윈도(window)를 무작위로 고릅니다. 동작을 보려면 이웃 프레임이 필요하므로 3D 합성곱에서 흔히 쓰입니다.
- 다중 클립(Multi-clip) — 같은 비디오에서 여러 개의 T-프레임 윈도를 샘플링하고, 테스트 시점에 예측(prediction)을 평균합니다.
T는 보통 8, 16, 32, 64입니다. T가 클수록 시간 신호(temporal signal)는 풍부해지고 연산량도 늘어납니다.
평가(Evaluation)
평가는 두 단계로 이루어집니다.
- 클립 단위 정확도(Clip-level accuracy) — 모델이 하나의 T-프레임 클립을 보고 top-k를 보고합니다.
- 비디오 단위 정확도(Video-level accuracy) — 한 비디오에서 여러 클립 단위 예측을 평균합니다. 더 높고 안정적입니다.
두 값을 항상 함께 보고합니다. 78% 클립 / 82% 비디오인 모델은 테스트 시점 평균화(test-time averaging)에 크게 의존하고 있다는 뜻입니다. 80% / 81%인 모델은 클립 단위에서도 더 견고(robust)합니다.
자주 만나는 데이터셋
- Kinetics-400 / 600 / 700 — 범용 행동 데이터셋입니다. 40만 클립 규모이고 YouTube URL 기반이라 이제는 끊긴 링크도 많습니다.
- Something-Something V2 — "X를 왼쪽에서 오른쪽으로 이동" 같은 동작 기반(motion-defined) 행동입니다. 2D+pool만으로는 풀기 어렵습니다.
- UCF-101, HMDB-51 — 오래되고 규모가 작지만 여전히 보고됩니다.
- AVA — 공간과 시간에서의 행동 위치 추정(action localisation)을 수행합니다. 분류보다 어렵습니다.
만들어보기
Step 1: 프레임 샘플러(Frame sampler)
프레임 리스트 또는 비디오 텐서(tensor) 위에서 동작하는 균일 샘플러와 밀집 샘플러입니다.
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))
두 함수 모두 비디오 텐서를 슬라이싱하는 데 사용할 T개의 인덱스를 반환합니다.
Step 2: 2D+pool 베이스라인
2D ResNet-18을 모든 프레임에 실행하고, 특징을 평균 풀링(average-pool)한 뒤 분류합니다.
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]))
self.head = nn.Linear(512, num_classes)
def forward(self, x):
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"출력: {model(x).shape}")
print(f"파라미터 수: {sum(p.numel() for p in model.parameters()):,}")
약 1100만 개의 파라미터(parameter)를 가지며 ImageNet 사전학습 가중치를 사용할 수 있습니다. 프레임별로 실행하고 평균을 낸 뒤 분류합니다. 이 베이스라인은 외형 중심의 과제에서 본격적인 3D 모델과 5~10 포인트 이내까지 따라붙는 경우가 많고, 더 강력한 ImageNet 백본(backbone)을 재사용한 덕분에 더 나은 결과를 낼 때도 있습니다.
Step 3: I3D 스타일 인플레이션 3D 합성곱
2D 합성곱 하나를 새 시간 축으로 가중치를 반복해 3D 합성곱으로 바꿉니다.
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)
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 가중치 모양: {tuple(conv2d.weight.shape)}")
print(f"3D 가중치 모양: {tuple(conv3d.weight.shape)}")
x = torch.randn(1, 3, 8, 56, 56)
print(f"3D 출력 모양: {tuple(conv3d(x).shape)}")
time_kernel로 나누는 이유는 활성값(activation)의 크기를 대략 유지하기 위해서입니다. 첫 번째 순전파(pass)에서 배치 정규화(batch-norm) 통계를 망가뜨리지 않으려면 이 보정이 중요합니다.
Step 4: 인수분해된 (2+1)D 합성곱
3D 합성곱을 2D 공간(spatial) 합성곱과 1D 시간(temporal) 합성곱으로 나눕니다. 수용 영역(receptive field)은 같지만 파라미터가 적고, 일부 벤치마크에서는 더 정확합니다.
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 출력: {tuple(c(x).shape)}")
전체 R(2+1)D 네트워크는 ResNet-18의 모든 3x3 합성곱을 Conv2Plus1D로 교체한 것과 같습니다.
활용하기
실제 운영 환경의 비디오 작업에는 다음 두 라이브러리가 주로 쓰입니다.
torchvision.models.video — R(2+1)D, MViT, Swin3D와 Kinetics에서 사전학습된 가중치를 제공합니다. 이미지 모델과 동일한 API를 사용합니다.
pytorchvideo(Meta) — 모델 모음(model zoo), Kinetics / SSv2 / AVA용 데이터 로더(dataloader), 표준 변환(transform)을 제공합니다.
비전-언어(Vision-Language) 비디오 모델, 예를 들어 비디오 캡셔닝(video captioning)이나 비디오 질의응답(video QA)에는 transformers 라이브러리의 VideoMAE, VideoLLaMA, InternVideo를 사용합니다.
산출물 만들기
이 레슨의 최종 산출물은 다음과 같습니다.
outputs/prompt-video-architecture-picker.md — 외형 대 동작, 데이터셋 규모, 연산 예산(compute budget)을 바탕으로 2D+pool / I3D / (2+1)D / 트랜스포머 중 하나를 고르는 프롬프트입니다.
outputs/skill-frame-sampler-auditor.md — 비디오 파이프라인의 샘플러를 검사하고 인덱스 어긋남(off-by-one index), num_frames < T 처리, 비율 보존 크롭(aspect-preserving crop) 누락 같은 버그를 표시하는 스킬입니다.
연습문제
- (쉬움) T=8인 FramePool과 T=8인 I3D 스타일 3D ResNet의 FLOPs를 대략 계산합니다. 왜 2D+pool이 3~5배 더 저렴한지 설명합니다.
- (중간) 무작위로 움직이는 공들이 들어 있는 합성 비디오 데이터셋을 만들고, 동작 방향(
left-to-right, right-to-left, diagonal-up)으로 라벨을 붙입니다. FramePool을 학습해 거의 무작위(near-chance) 수준의 정확도가 나오는 것을 보여 외형만으로는 동작 과제를 풀 수 없음을 증명합니다.
- (어려움) ResNet-18의 모든
Conv2d를 Conv2Plus1D로 바꿔 R(2+1)D-18을 구성합니다. ImageNet으로 사전학습된 ResNet-18에서 첫 합성곱 가중치를 인플레이션해 가져옵니다. 연습문제 2의 동작 데이터셋으로 학습해 FramePool을 이깁니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 2D + pool | "프레임 단위 분류기" | 샘플링된 프레임마다 2D CNN을 실행하고, 시간 방향으로 특징을 평균 풀링한 뒤 분류합니다. |
| 3D 합성곱(3D convolution) | "시공간 커널" | (T, H, W) 위에서 합성곱을 수행하는 커널입니다. 동작을 기본적으로 모델링합니다. |
| 인플레이션(Inflation) | "2D 가중치를 3D로 끌어올리기" | 2D 합성곱의 가중치를 새 시간 축으로 반복해 3D 합성곱을 초기화하고, 활성값 크기를 보존하기 위해 kernel_T로 나눕니다. |
| (2+1)D | "인수분해된 합성곱" | 3D 합성곱을 2D 공간 합성곱과 1D 시간 합성곱으로 분리합니다. 파라미터가 적고 중간에 비선형성(non-linearity)이 추가됩니다. |
| 분할 어텐션(Divided attention) | "시간 다음 공간" | 같은 위치의 토큰에 대한 시간 어텐션과 같은 프레임의 토큰에 대한 공간 어텐션을 레이어마다 나누어 적용합니다. |
| 클립(Clip) | "T-프레임 윈도" | T개의 프레임으로 샘플링된 부분 시퀀스입니다. 비디오 모델이 한 번에 소비하는 단위입니다. |
| 클립 정확도 대 비디오 정확도 | "두 가지 평가 설정" | 클립은 비디오당 한 샘플, 비디오는 여러 클립 예측의 평균입니다. |
| Kinetics | "비디오 분야의 ImageNet" | 400~700개의 행동 클래스와 30만 개 이상의 YouTube 클립을 가진 표준 비디오 사전학습 말뭉치(corpus)입니다. |
더 읽을거리