비전 트랜스포머 (Vision Transformers; ViT)

이미지를 패치(patch)로 자르고, 각 패치를 단어처럼 다루고, 표준 트랜스포머(transformer)를 실행합니다. 뒤돌아보지 않습니다.

유형: Build 언어: Python 선수 학습: Phase 7 Lesson 02 (Self-Attention), Phase 4 Lesson 04 (Image Classification) 소요 시간: 약 45분

학습 목표

  • 패치 임베딩(patch embedding), 학습 가능한 위치 임베딩(learned positional embedding), 클래스 토큰(class token), 트랜스포머 인코더 블록(transformer encoder block)을 직접 구현해 최소 형태의 ViT를 만듭니다.
  • ViT가 왜 DeiT와 MAE 이전에는 대규모 사전학습 데이터(pretraining data)가 필요하다고 여겨졌는지 설명합니다.
  • ViT, Swin, ConvNeXt의 구조적 사전 가정(architectural prior)을 비교합니다. 각각 사전 가정 없음, 지역 윈도우 어텐션(local window attention), 합성곱 백본(conv backbone)에 해당합니다.
  • timm을 사용해 사전학습된 ViT를 작은 데이터셋(dataset)에 미세 조정(fine-tuning)하고 표준 선형 탐침/미세 조정(linear-probe / fine-tune) 레시피를 적용합니다.

문제

지난 10년 동안 합성곱(convolution)은 컴퓨터 비전(computer vision)과 거의 동의어였습니다. CNN은 지역성(locality)과 이동 등변성(translation equivariance)이라는 강한 귀납적 편향(inductive bias)을 가지고 있었고, 이를 대체할 수 있다고 생각한 사람은 많지 않았습니다. 그러다 Dosovitskiy et al.(2020)은 평탄화된 이미지 패치(flattened image patch)에 합성곱 장치가 전혀 없는 평범한 트랜스포머를 적용해도, 규모(scale)가 충분하다면 최고 수준의 CNN과 비슷하거나 더 나은 성능을 낼 수 있음을 보였습니다.

문제는 바로 "규모가 충분하다면"이라는 단서였습니다. ImageNet-1k에서 ViT는 ResNet에 졌습니다. 하지만 ImageNet-21k나 JFT-300M에서 사전학습(pretrain)한 뒤 ImageNet-1k에서 미세 조정하면 이겼습니다. 결론은 트랜스포머에는 유용한 사전 가정이 부족하지만, 데이터가 충분하면 이를 데이터에서 배울 수 있다는 것이었습니다. 이후 DeiT, MAE, DINO는 강한 데이터 증강(augmentation), 자기지도 사전학습(self-supervised pretraining), 지식 증류(distillation) 같은 올바른 학습 레시피(training recipe)가 있으면 ViT도 작은 데이터에서 잘 학습된다는 것을 보였습니다.

2026년에도 엣지 디바이스(edge device)에서는 순수 CNN이 여전히 경쟁력이 있습니다. 특히 ConvNeXt가 강합니다. 하지만 나머지 대부분 영역에서는 트랜스포머가 지배적입니다. 분할(segmentation; Mask2Former, SegFormer), 객체 검출(detection; DETR, RT-DETR), 멀티모달(multimodal; CLIP, SigLIP), 비디오(video; VideoMAE, VJEPA)가 모두 그렇습니다. 반드시 알아야 할 핵심 구조는 ViT 블록입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.ViT는 이미지를 어떻게 토큰 시퀀스(token sequence)로 바꾸나요?

2.ViT의 [CLS] 토큰은 무엇이고 왜 필요한가요?

0/2 답변 완료

개념

파이프라인(Pipeline)

flowchart LR
    IMG["Image<br/>(3, 224, 224)"] --> PATCH["Patch embedding<br/>conv 16x16 s=16<br/>-> (768, 14, 14)"]
    PATCH --> FLAT["Flatten to<br/>(196, 768) tokens"]
    FLAT --> CAT["Prepend<br/>[CLS] token"]
    CAT --> POS["Add learned<br/>positional embed"]
    POS --> ENC["N transformer<br/>encoder blocks"]
    ENC --> CLS["Take [CLS]<br/>token output"]
    CLS --> HEAD["MLP classifier"]

    style PATCH fill:#dbeafe,stroke:#2563eb
    style ENC fill:#fef3c7,stroke:#d97706
    style HEAD fill:#dcfce7,stroke:#16a34a

총 일곱 단계입니다. 패치 → 토큰 → 어텐션 → 분류기로 흐릅니다. DeiT, Swin, ConvNeXt, MAE 사전학습 같은 모든 변형(variant)은 이 일곱 단계 중 한두 개만 바꾸고 나머지는 그대로 유지합니다.

패치 임베딩(Patch embedding)

첫 합성곱 연산이 핵심입니다. 커널 크기(kernel size) 16, 스트라이드(stride) 16이므로 224x224 이미지는 16x16 패치들의 14x14 격자(grid)가 되고, 각 패치는 768차원 임베딩(embedding)으로 투영(projection)됩니다. 이 단일 합성곱이 패치화(patchify)와 선형 투영(linear projection)을 동시에 수행합니다.

Input:  (3, 224, 224)
Conv (3 -> 768, k=16, s=16, no padding):
Output: (768, 14, 14)
Flatten spatial: (196, 768)

196개 패치 = 196개 토큰입니다. 각 토큰의 특징 차원(feature dimension)은 ViT-B에서는 768, ViT-L에서는 1024, ViT-H에서는 1280입니다.

클래스 토큰(Class token)

시퀀스(sequence) 맨 앞에 붙는 하나의 학습 가능한 벡터(learned vector)입니다.

tokens = [CLS; patch_1; patch_2; ...; patch_196]   shape (197, 768)

N개의 트랜스포머 블록을 지나면 [CLS]의 출력이 이미지 전체에 대한 전역 표현(global image representation)이 됩니다. 분류 헤드(classification head)는 이 벡터 하나만 읽습니다.

위치 임베딩(Positional embedding)

트랜스포머는 공간적 위치(spatial position)를 기본적으로 알지 못합니다. 따라서 모든 토큰에 학습 가능한 벡터를 더해줍니다.

tokens = tokens + learned_pos_embedding   (also shape (197, 768))

이 임베딩은 모델 파라미터(parameter)이고, 경사 기반 학습(gradient-based training)이 이를 2차원 이미지 구조에 맞게 조정합니다. 사인파 형태의 2차원 대안(sinusoidal 2D alternative)도 있지만 실무에서는 학습 가능한 위치 임베딩이 더 흔하게 쓰입니다.

트랜스포머 인코더 블록(Transformer encoder block)

표준 구조입니다. 다중 헤드 셀프 어텐션(multi-head self-attention), MLP, 잔차 연결(residual connection), 사전 LayerNorm(pre-LayerNorm)으로 구성됩니다.

x = x + MSA(LN(x))
x = x + MLP(LN(x))

MLP is two-layer with GELU: Linear(d -> 4d) -> GELU -> Linear(4d -> d)

ViT-B/16은 이런 블록 12개를 쌓고, 각 블록에는 어텐션 헤드(attention head)가 12개 들어 있으며, 전체 파라미터는 약 86M입니다.

Pre-LN을 쓰는 이유

초기 트랜스포머는 사후 LayerNorm(post-LN; x = LN(x + sublayer(x)))을 사용했고, 학습률 워밍업(warmup) 없이는 6~8 레이어를 넘어 안정적으로 학습하기 어려웠습니다. 사전 LayerNorm(pre-LN; x = x + sublayer(LN(x)))은 깊은 신경망을 워밍업 없이도 안정적으로 학습시킵니다. 모든 ViT와 현대 LLM은 pre-LN을 사용합니다.

패치 크기 절충(Patch size trade-off)

  • 16x16 패치 → 196개 토큰, 표준 설정입니다.
  • 32x32 패치 → 49개 토큰, 빠르지만 해상도가 낮습니다.
  • 8x8 패치 → 784개 토큰, 더 섬세하지만 어텐션 비용(attention cost)이 O(n^2)로 증가해 불리해집니다.

큰 패치는 토큰 수가 적어 빠르지만 공간 디테일(spatial detail)을 잃습니다. SwinV2는 계층적 윈도우(hierarchical window) 안에서 4x4 패치를 사용합니다.

ImageNet-1k에서 ViT를 학습시킨 DeiT 레시피

원래 ViT는 CNN을 이기기 위해 JFT-300M이 필요했습니다. DeiT(Touvron et al., 2020)는 네 가지 변화만으로 ImageNet-1k만 사용해 ViT-B top-1 81.8%를 달성했습니다.

  1. 강한 데이터 증강(heavy augmentation): RandAugment, Mixup, CutMix, Random Erasing.
  2. 확률적 깊이(stochastic depth): 학습 중 블록 전체를 무작위로 떨어뜨립니다(drop).
  3. 반복 증강(repeated augmentation): 같은 이미지를 배치(batch)마다 3번 샘플링합니다.
  4. CNN 교사로부터의 지식 증류(distillation from a CNN teacher): 선택 사항이지만 정확도를 더 끌어올립니다.

현대 ViT 학습 레시피는 거의 모두 DeiT에서 이어집니다.

Swin과 ConvNeXt 비교

  • Swin(Liu et al., 2021) — 윈도우 기반 어텐션(window-based attention)을 사용합니다. 각 블록은 지역 윈도우 안에서 어텐션을 수행하고, 교대로 등장하는 블록은 윈도우를 이동(shift)시켜 윈도우 사이의 정보를 섞습니다. 어텐션 연산자(attention operator)는 유지하면서 CNN과 비슷한 지역성 사전 가정을 다시 도입한 방식입니다.
  • ConvNeXt(Liu et al., 2022) — Swin의 구조 선택(깊이별 합성곱(depthwise conv), LayerNorm, GELU, 역병목(inverted bottleneck))에 맞춰 다시 설계된 CNN입니다. 성능 격차가 "어텐션 대 합성곱"이 아니라 "현대적 학습 레시피 + 구조"에서 비롯됨을 보여주었습니다.

2026년 기준 ConvNeXt-V2와 Swin-V2는 모두 프로덕션(production)에서 쓸 수 있는 수준입니다. 어느 쪽을 택할지는 추론 스택(inference stack), 엣지 컴파일(edge compilation)에 ConvNeXt가 더 유리한지 여부, 그리고 사전학습 코퍼스(pretraining corpus)에 따라 달라집니다.

MAE 사전학습

마스킹된 오토인코더(Masked Autoencoder; MAE)(He et al., 2022)는 패치의 75%를 무작위로 마스킹(mask)하고, 인코더(encoder)는 보이는 25%만 처리하게 하며, 작은 디코더(decoder)가 마스킹된 패치를 복원(reconstruct)하도록 학습합니다. 사전학습이 끝나면 디코더는 버리고 인코더만 미세 조정합니다.

MAE는 ViT를 ImageNet-1k만으로도 잘 학습되게 만들었고, SOTA를 달성했으며, 현재 기본적으로 쓰이는 자기지도 학습 레시피 중 하나입니다.

만들어보기

Step 1: 패치 임베딩

import torch
import torch.nn as nn

class PatchEmbedding(nn.Module):
    def __init__(self, in_channels=3, patch_size=16, dim=192, image_size=64):
        super().__init__()
        assert image_size % patch_size == 0
        self.proj = nn.Conv2d(in_channels, dim, kernel_size=patch_size, stride=patch_size)
        num_patches = (image_size // patch_size) ** 2
        self.num_patches = num_patches

    def forward(self, x):
        x = self.proj(x)
        return x.flatten(2).transpose(1, 2)

합성곱 한 번, 평탄화(flatten) 한 번, 전치(transpose) 한 번이 이미지에서 토큰으로 가는 전체 단계입니다.

Step 2: 트랜스포머 블록

pre-LN, 다중 헤드 셀프 어텐션, GELU를 사용하는 MLP, 잔차 연결로 구성됩니다.

class Block(nn.Module):
    def __init__(self, dim, num_heads, mlp_ratio=4, dropout=0.0):
        super().__init__()
        self.ln1 = nn.LayerNorm(dim)
        self.attn = nn.MultiheadAttention(dim, num_heads, dropout=dropout, batch_first=True)
        self.ln2 = nn.LayerNorm(dim)
        self.mlp = nn.Sequential(
            nn.Linear(dim, dim * mlp_ratio),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(dim * mlp_ratio, dim),
            nn.Dropout(dropout),
        )

    def forward(self, x):
        a, _ = self.attn(self.ln1(x), self.ln1(x), self.ln1(x), need_weights=False)
        x = x + a
        x = x + self.mlp(self.ln2(x))
        return x

nn.MultiheadAttention은 헤드 분할, 스케일드 내적(scaled dot-product), 출력 투영(output projection)을 처리합니다. batch_first=True이므로 텐서 형상(shape)은 (N, seq, dim)입니다.

Step 3: ViT 본체

class ViT(nn.Module):
    def __init__(self, image_size=64, patch_size=16, in_channels=3,
                 num_classes=10, dim=192, depth=6, num_heads=3, mlp_ratio=4):
        super().__init__()
        self.patch = PatchEmbedding(in_channels, patch_size, dim, image_size)
        num_patches = self.patch.num_patches
        self.cls_token = nn.Parameter(torch.zeros(1, 1, dim))
        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, dim))
        self.blocks = nn.ModuleList([
            Block(dim, num_heads, mlp_ratio) for _ in range(depth)
        ])
        self.ln = nn.LayerNorm(dim)
        self.head = nn.Linear(dim, num_classes)
        nn.init.trunc_normal_(self.pos_embed, std=0.02)
        nn.init.trunc_normal_(self.cls_token, std=0.02)

    def forward(self, x):
        x = self.patch(x)
        cls = self.cls_token.expand(x.size(0), -1, -1)
        x = torch.cat([cls, x], dim=1)
        x = x + self.pos_embed
        for blk in self.blocks:
            x = blk(x)
        x = self.ln(x[:, 0])
        return self.head(x)

vit = ViT(image_size=64, patch_size=16, num_classes=10, dim=192, depth=6, num_heads=3)
x = torch.randn(2, 3, 64, 64)
print(f"출력: {vit(x).shape}")
print(f"파라미터 수: {sum(p.numel() for p in vit.parameters()):,}")

약 280만 개 파라미터를 가진 작은 ViT입니다. CPU에서도 다룰 수 있습니다. 실제 ViT-B는 dim=768, depth=12, num_heads=12 설정을 쓰며, 같은 클래스 정의로 약 86M개 파라미터가 됩니다.

Step 4: 동작 점검(Sanity check) — 단일 이미지 추론

logits = vit(torch.randn(1, 3, 64, 64))
print(f"로짓: {logits}")
print(f"확률:  {logits.softmax(-1)}")

오류 없이 실행되어야 합니다. 출력된 확률(probability)의 합은 1이 됩니다.

활용하기

timm은 ImageNet에서 사전학습된 가중치(weight)와 함께 사실상 거의 모든 ViT 변형을 제공합니다. 한 줄이면 됩니다.

import timm

model = timm.create_model("vit_base_patch16_224", pretrained=True, num_classes=10)

timm은 2026년 기준 비전 트랜스포머의 프로덕션 표준 라이브러리입니다. ViT, DeiT, Swin, Swin-V2, ConvNeXt, ConvNeXt-V2, MaxViT, MViT, EfficientFormer 등 수많은 모델을 동일한 API로 지원합니다.

이미지와 텍스트를 함께 다루는 멀티모달 작업에는 transformers의 CLIP, SigLIP, BLIP-2, LLaVA를 사용합니다. 이들의 이미지 인코더는 모두 ViT 변형입니다.

산출물 만들기

이 레슨의 최종 산출물은 다음과 같습니다.

  • outputs/prompt-vit-vs-cnn-picker.md — 데이터셋 크기, 연산 자원(compute), 추론 스택을 바탕으로 ViT, ConvNeXt, Swin 중 하나를 고르도록 도와주는 프롬프트입니다.
  • outputs/skill-vit-patch-and-pos-embed-inspector.md — ViT의 패치 임베딩과 위치 임베딩 형상(shape)이 모델이 기대하는 시퀀스 길이(sequence length)와 일치하는지 검증해 흔히 발생하는 포팅(porting) 버그를 잡는 스킬입니다.

연습문제

  1. (쉬움) 위 작은 ViT의 순전파(forward pass) 과정에서 모든 중간 텐서의 형상을 출력해 봅니다. 입력 (N, 3, 64, 64) → 패치 (N, 16, 192) → CLS 포함 (N, 17, 192) → 분류기 입력 (N, 192) → 출력 (N, num_classes) 순서를 확인합니다.
  2. (중간) Lesson 4의 합성 CIFAR(synthetic-CIFAR) 데이터셋에서 사전학습된 timm ViT-S/16을 미세 조정합니다. 같은 데이터에서 ResNet-18 미세 조정 결과와 비교하고, 학습 시간(training time)과 최종 정확도(final accuracy)를 보고합니다.
  3. (어려움) 작은 ViT에 대해 MAE 사전학습을 직접 구현합니다. 패치의 75%를 마스킹하고, 인코더와 작은 디코더가 마스킹된 패치를 복원하도록 학습합니다. 사전학습 전후로 합성 데이터에서의 선형 탐침(linear-probe) 정확도를 평가합니다.

핵심 용어

용어흔한 설명실제 의미
패치 임베딩(Patch embedding)"첫 합성곱"커널 크기 = 스트라이드 = 패치 크기인 합성곱입니다. 이미지를 토큰 임베딩 격자로 바꿉니다.
클래스 토큰(Class token)"[CLS]"토큰 시퀀스 앞에 붙는 학습 가능한 벡터입니다. 최종 출력이 이미지 전체의 전역 표현이 됩니다.
위치 임베딩(Positional embedding)"학습된 위치(learned pos)"트랜스포머가 각 패치의 위치를 알 수 있도록 모든 토큰에 더하는 학습 가능한 벡터입니다.
Pre-LN"서브 레이어 앞의 LayerNorm"안정적인 트랜스포머 변형입니다. LN(x + sublayer(x)) 대신 x + sublayer(LN(x))를 씁니다.
다중 헤드 어텐션(Multi-head attention)"병렬 어텐션"표준 트랜스포머 어텐션을 여러 독립적인 부분 공간(subspace)으로 나눈 뒤 다시 이어붙이는(concat) 방식입니다.
ViT-B/16"Base, 패치 16"정규(canonical) 크기 설정입니다. dim=768, depth=12, heads=12, patch_size=16, image=224, 약 86M 파라미터입니다.
DeiT"데이터 효율적인 ViT"강한 데이터 증강을 사용해 ImageNet-1k만으로 학습한 ViT입니다. 대규모 사전학습 데이터셋이 절대 필수는 아님을 보였습니다.
MAE"마스킹된 오토인코더"패치의 75%를 마스킹하고 복원하는 자기지도 사전학습 방식입니다. 현재 ViT 사전학습에서 가장 지배적인 레시피입니다.

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 2개

skill-vit-patch-and-pos-embed-inspector

Verify a ViT's patch embedding and positional embedding shapes match the model's expected sequence length

Skill
prompt-vit-vs-cnn-picker

Pick between ViT, ConvNeXt, or Swin based on dataset size, compute, and inference stack

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.원래 ViT 논문에서는 ResNet을 이기기 위해 JFT-300M 사전학습이 필요했는데, DeiT는 왜 그것이 필요하지 않았나요?

2.사전 LayerNorm(pre-LN; `x = x + sublayer(LN(x))`)과 사후 LayerNorm(post-LN; `x = LN(x + sublayer(x))`) 중 현대 트랜스포머는 무엇을 사용하고, 그 이유는 무엇인가요?

3.Swin 트랜스포머는 윈도우 어텐션(windowed attention)을 도입합니다. 어떤 문제를 해결하나요?

0/3 답변 완료