자기지도 비전(Self-Supervised Vision) — SimCLR, DINO, MAE
라벨(label)은 지도 비전(supervised vision)의 병목(bottleneck)입니다. 자기지도 사전학습(self-supervised pretraining)은 그 병목을 없앱니다. 라벨이 없는 이미지 1억 장에서 시각 특징(visual feature)을 배우고, 라벨이 붙은 이미지 1만 장으로 미세조정(fine-tune)합니다.
유형: Learn + Build
언어: Python
선수 학습: Phase 4 Lesson 04 (이미지 분류), Phase 4 Lesson 14 (ViT)
소요 시간: 약 75분
학습 목표
- 자기지도 학습의 세 가지 주요 계열(self-supervised family) — 대조 학습(contrastive; SimCLR), 교사-학생(teacher-student; DINO), 마스킹 복원(masked reconstruction; MAE) — 의 흐름을 따라가고 각 계열이 무엇을 최적화하는지 설명합니다.
- InfoNCE 손실(InfoNCE loss)을 직접 구현하고, 배치 크기 512는 잘 작동하지만 배치 크기 32는 실패하는 이유를 설명합니다.
- MAE의 75% 마스킹 비율(masking ratio)이 임의의 값이 아니며, 텍스트용 BERT가 사용하는 15%와 어떻게 다른지 설명합니다.
- DINOv2 또는 MAE의 ImageNet 체크포인트(checkpoint)를 선형 프로빙(linear probing)과 제로샷 검색(zero-shot retrieval)에 활용합니다.
문제
지도학습용 ImageNet에는 라벨이 붙은 이미지 130만 장이 있으며, 그 주석(annotation) 비용은 약 1000만 달러로 추정됩니다. 의료 및 산업 데이터셋(dataset)은 규모가 더 작고 라벨링 비용은 더 비쌉니다. 그래서 모든 비전 팀은 같은 질문을 던집니다. 유튜브(YouTube) 프레임, 웹 크롤(web crawl), 웹캠 영상, 위성 촬영 같은 저렴한 비라벨 데이터로 사전학습한 뒤, 작은 라벨 데이터셋에서 미세조정할 수 있을까요?
자기지도 학습(self-supervised learning)이 그 답입니다. LAION이나 JFT 같은 데이터로 학습한 현대적인 자기지도 ViT는 미세조정 후 지도학습 ImageNet의 정확도(accuracy)에 도달하거나 이를 넘어섭니다. 객체 탐지(detection), 분할(segmentation), 깊이 추정(depth) 같은 후속 과제(downstream task)로의 전이(transfer) 성능도 지도 사전학습보다 우수합니다. DINOv2(Meta, 2023)와 MAE(Meta, 2022)는 전이 가능한 비전 특징을 얻기 위한 현재의 프로덕션 기본값입니다.
여기서 일어나는 개념적 전환은, 모델이 학습 중 풀어야 하는 사전 과제(pretext task)가 후속 과제와 같을 필요가 없다는 점입니다. 중요한 것은 그 사전 과제가 모델로 하여금 유용한 특징을 학습하도록 강제하는가입니다. 회색조 이미지의 색상을 예측하기, 이미지를 회전시켜 회전 각도를 분류하기, 패치(patch)를 가리고 복원하기 등이 모두 효과가 있었습니다. 이 가운데 규모 확장에 성공한 세 가지 접근이 대조 학습, 교사-학생 증류(teacher-student distillation), 마스킹 복원입니다.
개념
세 가지 계열(families)
flowchart LR
A["대조 학습<br/>SimCLR, MoCo, CLIP"] --> AT["양성 쌍(positive pair)<br/>(같은 이미지, 증강 2개)<br/>는 가깝게,<br/>음성(negative)은 멀게"]
B["교사-학생<br/>DINO, BYOL, iBOT"] --> BT["학생(student)이<br/>교사(teacher) 출력을 예측;<br/>교사는 학생의 EMA"]
C["마스킹 복원<br/>MAE, BEiT, SimMIM"] --> CT["패치의 75%를 가리고;<br/>픽셀 또는 토큰 타깃을<br/>복원"]
style A fill:#dbeafe,stroke:#2563eb
style B fill:#fef3c7,stroke:#d97706
style C fill:#dcfce7,stroke:#16a34a
대조 학습(contrastive learning; SimCLR)
한 장의 이미지에 두 개의 무작위 증강(random augmentation)을 적용해 두 개의 뷰(view)를 만듭니다. 두 뷰를 같은 인코더(encoder)와 사영 헤드(projection head)에 통과시킵니다. 그리고 "이 두 임베딩(embedding)은 가깝게", "이 임베딩은 같은 배치 안의 다른 모든 이미지 임베딩과는 멀게" 만드는 손실(loss)을 최소화합니다.
배치당 2N개의 뷰가 있을 때 양성 쌍 (z_i, z_j)에 대한 손실:
L_ij = -log( exp(sim(z_i, z_j) / tau) / sum_k in batch \ {i} exp(sim(z_i, z_k) / tau) )
sim = 코사인 유사도(cosine similarity)
tau = 온도(temperature, 표준값 0.1)
이것이 바로 InfoNCE 손실입니다. 양성 쌍 하나에 대해 많은 음성 예시(negative)가 필요하기 때문에 배치 크기(batch size)가 중요합니다. SimCLR은 배치 크기 512~8192를 요구합니다. MoCo는 과거 배치들의 모멘텀 큐(momentum queue)를 도입해, 음성 개수를 배치 크기와 분리했습니다.
교사-학생(teacher-student; DINO)
같은 아키텍처(architecture)를 가진 두 개의 네트워크, 학생(student)과 교사(teacher)를 둡니다. 교사의 가중치(weight)는 학생 가중치의 지수 이동 평균(exponential moving average; EMA)입니다. 두 네트워크 모두 이미지의 증강된 뷰를 입력으로 받습니다. 학생의 출력은 교사의 출력을 따라가도록 학습되며, 명시적인 음성 예시는 사용하지 않습니다.
loss = CE( student_output(view_1), teacher_output(view_2) )
+ CE( student_output(view_2), teacher_output(view_1) )
teacher_weights = m * teacher_weights + (1 - m) * student_weights (m ≈ 0.996)
학생이 "항상 같은 상수를 출력"하는 식으로 붕괴(collapse)하지 않는 이유는, 교사 출력에 중심화(centering; 차원별 평균을 빼는 처리)와 선명화(sharpening; 낮은 온도로 나누는 처리)를 적용하기 때문입니다. 중심화는 한 차원(dimension)이 출력 전체를 지배하지 못하도록 막고, 선명화는 출력이 균등 분포(uniform)로 무너지는 것을 막습니다.
DINO는 DINOv2가 선별된 이미지 1억 4200만 장으로 규모를 키운 기반입니다. 그 결과로 얻은 특징은 제로샷 시각 검색(zero-shot visual retrieval)과 조밀 예측(dense prediction)에서 현재 최상위(SOTA) 수준입니다.
마스킹 복원(masked reconstruction; MAE)
ViT 입력 패치의 75%를 가립니다(mask). 인코더에는 보이는 25%의 패치만 통과시킵니다. 작은 디코더(decoder)는 인코더의 출력과, 가려진 위치에 들어갈 마스크 토큰(mask token)을 함께 받아, 가려진 패치의 픽셀을 복원하도록 학습됩니다.
인코더: 보이는 패치 25% -> 특징
디코더: 특징 + 가려진 위치의 마스크 토큰 -> 복원된 픽셀
손실: 가려진 패치에 한해서만 복원 픽셀과 원본 픽셀의 MSE
MAE를 동작하게 만드는 핵심 설계는 다음과 같습니다.
- 75% 마스킹 비율 — 매우 높은 값입니다. 인코더가 의미적 특징(semantic feature)을 학습하도록 강제합니다. 25%만 가리면 이웃 픽셀의 강한 상관관계 때문에 복원이 거의 자명한(trivial) 문제가 되어버립니다(이웃 픽셀이 서로 매우 닮아 있어 CNN으로도 풀립니다).
- 비대칭 인코더/디코더(asymmetric encoder/decoder) — 큰 ViT 인코더는 보이는 패치만 처리합니다. 작고(예: 8층, 512차원) 가벼운 디코더가 복원을 담당합니다. 그 결과 단순(naive) BEiT 대비 사전학습 속도가 약 3배 빨라집니다.
- 픽셀 공간 복원 타깃(pixel-space reconstruction target) — BEiT가 사용하는 토큰화된 타깃(tokenised target)보다 단순하면서도 ViT에서 더 좋은 결과를 냅니다.
사전학습이 끝나면 디코더는 버립니다. 인코더가 특징 추출기(feature extractor)가 됩니다.
왜 75%이고 15%가 아닌가
BERT는 토큰의 15%를 가리고, MAE는 패치의 75%를 가립니다. 이 차이는 정보 밀도(information density)에서 비롯됩니다.
- 자연어는 토큰당 엔트로피(entropy)가 높습니다. 15%의 토큰을 예측하는 것도 어렵습니다. 가려진 자리마다 그럴듯한 후보(plausible completion)가 많기 때문입니다.
- 이미지 패치는 엔트로피가 낮습니다. 가려지지 않은 주변 영역만으로도 가려진 패치의 픽셀이 거의 결정됩니다. 그래서 의미 이해(semantic understanding)가 필요해지도록 만들려면 공격적으로 가리는(aggressive masking) 수밖에 없습니다.
75%는 단순한 공간 외삽(spatial extrapolation)만으로는 풀 수 없을 만큼 높고, 그래서 인코더가 이미지의 내용을 표현하도록 강제합니다.
선형 프로브 평가(linear-probe evaluation)
자기지도 사전학습이 끝난 뒤 표준 평가는 선형 프로브(linear probe)입니다. 인코더를 동결(freeze)하고 그 위에 단일 선형 분류기(linear classifier) 하나만 ImageNet 라벨로 학습합니다. 그리고 상위-1 정확도(top-1 accuracy)를 보고합니다.
- SimCLR ResNet-50: 약 71% (2020)
- DINO ViT-S/16: 약 77% (2021)
- MAE ViT-L/16: 약 76% (2022)
- DINOv2 ViT-g/14: 약 86% (2023)
선형 프로브는 특징 품질(feature quality)에 가장 가까운 순수 측정치입니다. 미세조정은 일반적으로 2~5%p 정도 정확도를 더 올려주지만, 분류 헤드 재학습(head retraining) 효과까지 함께 섞입니다.
만들어보기
Step 1: 두 뷰 증강 파이프라인(two-view augmentation pipeline)
import torch
import torchvision.transforms as T
two_view_train = lambda: T.Compose([
T.RandomResizedCrop(96, scale=(0.2, 1.0)),
T.RandomHorizontalFlip(),
T.ColorJitter(0.4, 0.4, 0.4, 0.1),
T.RandomGrayscale(p=0.2),
T.ToTensor(),
])
class TwoViewDataset(torch.utils.data.Dataset):
def __init__(self, base):
self.base = base
self.aug = two_view_train()
def __len__(self):
return len(self.base)
def __getitem__(self, i):
img, _ = self.base[i]
v1 = self.aug(img)
v2 = self.aug(img)
return v1, v2
각 __getitem__ 호출은 같은 이미지에서 만들어진 두 개의 증강 뷰를 반환합니다. 라벨은 필요 없습니다.
Step 2: InfoNCE 손실
import torch.nn.functional as F
def info_nce(z1, z2, tau=0.1):
"""
z1, z2: 짝지어진 뷰에 대한 (N, D) 모양의 L2 정규화된 임베딩
"""
N, D = z1.shape
z = torch.cat([z1, z2], dim=0)
sim = z @ z.T / tau
mask = torch.eye(2 * N, dtype=torch.bool, device=z.device)
sim = sim.masked_fill(mask, float("-inf"))
targets = torch.cat([torch.arange(N, 2 * N), torch.arange(0, N)]).to(z.device)
return F.cross_entropy(sim, targets)
호출 전 임베딩은 L2 정규화(L2-normalise)된 상태여야 합니다. tau=0.1은 SimCLR의 기본값이며, 값을 더 낮추면 손실이 더 날카로워(sharp)지고 더 많은 음성 예시가 필요해집니다.
Step 3: InfoNCE 동작 확인(sanity check)
z1 = F.normalize(torch.randn(16, 32), dim=-1)
z2 = z1.clone()
loss_same = info_nce(z1, z2, tau=0.1).item()
z2_random = F.normalize(torch.randn(16, 32), dim=-1)
loss_random = info_nce(z1, z2_random, tau=0.1).item()
print(f"동일한 쌍의 InfoNCE: {loss_same:.3f}")
print(f"무작위 쌍의 InfoNCE: {loss_random:.3f}")
동일한 쌍은 낮은 손실을 가져야 합니다(배치가 크고 온도가 낮을수록 0에 가까워집니다). 무작위 쌍은 쌍 16개짜리 배치에서 log(2N-1) = log(31) ≈ 3.4에 가까운 값이 나와야 합니다.
Step 4: MAE 방식의 마스킹(MAE-style masking)
def random_mask_indices(num_patches, mask_ratio=0.75, seed=0):
g = torch.Generator().manual_seed(seed)
n_keep = int(num_patches * (1 - mask_ratio))
perm = torch.randperm(num_patches, generator=g)
visible = perm[:n_keep]
masked = perm[n_keep:]
return visible.sort().values, masked.sort().values
num_patches = 196
visible, masked = random_mask_indices(num_patches, mask_ratio=0.75)
print(f"보이는 패치: {len(visible)} / {num_patches}")
print(f"가려진 패치: {len(masked)} / {num_patches}")
단순하고 빠르며, 같은 시드(seed)에서는 결정적(deterministic)입니다. 실제 MAE 구현은 이 마스킹을 배치 단위로 처리하고 표본(sample)별 마스크를 별도로 유지합니다.
활용하기
2026년 기준 DINOv2는 프로덕션 표준(production standard)입니다.
import torch
from transformers import AutoImageProcessor, AutoModel
processor = AutoImageProcessor.from_pretrained("facebook/dinov2-base")
model = AutoModel.from_pretrained("facebook/dinov2-base")
model.eval()
with torch.no_grad():
inputs = processor(images=[pil_image], return_tensors="pt")
outputs = model(**inputs)
embedding = outputs.last_hidden_state[:, 0]
여기서 얻는 768차원 임베딩은 현대 이미지 검색(image retrieval), 조밀 대응(dense correspondence), 제로샷 전이 파이프라인(zero-shot transfer pipeline)의 기본 골격(backbone)입니다. 후속 과제에 미세조정할 때도 선형 헤드(linear head) 이상은 거의 필요하지 않습니다.
이미지-텍스트 임베딩(image-text embedding)이 필요하면 SigLIP 또는 OpenCLIP이 같은 자리를 차지합니다. MAE 방식의 미세조정에는 timm 저장소가 거의 모든 MAE 체크포인트를 제공합니다.
산출물 만들기
이 레슨의 산출물은 다음과 같습니다.
outputs/prompt-ssl-pretraining-picker.md — 데이터셋 크기, 연산 자원(compute), 후속 과제에 따라 SimCLR / MAE / DINOv2 중 무엇을 쓸지 골라주는 프롬프트(prompt)입니다.
outputs/skill-linear-probe-runner.md — 임의의 동결된 인코더와 라벨이 붙은 데이터셋에 대해 선형 프로브 평가를 작성해 주는 스킬(skill)입니다.
연습문제
- (쉬움) 잘 정렬된(aligned) 임베딩에서는 온도를 낮추면 InfoNCE 손실이 떨어지고, 무작위 임베딩에서는 온도를 낮추면 손실이 오른다는 사실을 확인합니다.
tau in [0.05, 0.1, 0.2, 0.5]에 대해 손실을 그린 그래프(plot)를 만듭니다.
- (중간) DINO 방식의 중심화 버퍼(centre buffer)를 구현합니다. 중심화가 없으면 학생이 몇 에폭(epoch) 안에 상수 벡터로 붕괴함을 보입니다.
- (어려움) Lesson 10의 TinyUNet을 백본(backbone)으로 사용해 CIFAR-100에서 MAE를 학습합니다. 10, 50, 200 에폭 시점에서 선형 프로브 정확도를 보고합니다. 같은 1,000장짜리 부분집합(subset)에서 MAE로 사전학습한 선형 프로브가 처음부터 지도학습한(from-scratch supervised) 선형 프로브를 이기는지 확인합니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 자기지도(self-supervised) | "라벨 없음(label-free)" | 라벨이 없는 데이터에서 유용한 표현(representation)을 만들어 내는 사전 과제(pretext task). |
| 사전 과제(pretext task) | "가짜 과제(fake task)" | 자기지도 학습(SSL) 중 사용하는 목적 함수(objective). 패치 복원, 뷰 맞추기 등이 있으며, 사전학습 후에는 폐기한다. |
| 선형 프로브(linear probe) | "동결된 인코더 + 선형 헤드" | 자기지도 학습의 표준 평가. 동결된 특징 위에 선형 분류기만 학습한다. |
| InfoNCE | "대조 손실(contrastive loss)" | 코사인 유사도 위에 적용되는 소프트맥스(softmax) 손실. 양성 쌍이 정답 클래스이고 나머지는 음성이다. |
| EMA 교사(EMA teacher) | "이동 평균 교사(moving-average teacher)" | 학생 가중치의 지수 이동 평균을 가중치로 사용하는 교사. BYOL, MoCo, DINO에서 사용한다. |
| 마스킹 비율(mask ratio) | "가려진 패치 비율" | MAE에서 가리는 패치 비율. 비전은 75%, 텍스트는 15%가 일반적이다. |
| 표현 붕괴(representation collapse) | "상수 출력(constant output)" | 인코더가 모든 입력에 대해 같은 벡터를 출력하는 자기지도 학습의 실패 모드. 중심화, 선명화, 음성 예시 등이 이를 막는다. |
| DINOv2 | "프로덕션 자기지도 백본" | Meta의 2023년 자기지도 ViT. 2026년 기준 가장 강력한 범용 이미지 특징을 제공한다. |
더 읽을거리