비전·음성·추천

DOMAIN / 17번째 글

Vision Transformer(ViT): 이미지를 문장처럼 처리하는 Transformer

패치 크기가 어텐션 비용을 어떻게 정하는지, 귀납 편향이 없다는 말이 데이터 요구량으로 어떻게 돌아오는지, 위치 임베딩 보간·DeiT·MAE·Swin이 각각 무엇을 되찾는지를 수치와 함께 짚는다.

PALDYN Team28 MIN READ

지난 글에서 한국어를 형태소 단위로 잘라 토큰 시퀀스로 만들고, 그 시퀀스를 KoBERT·KoELECTRA 같은 Transformer 모델에 넣는 과정을 살펴봤다. 입력을 잘게 쪼개 시퀀스로 세운 뒤 나머지를 어텐션에 맡기는 이 방식은 언어에만 쓸 수 있는 것이 아니다. 이번 글에서는 같은 발상을 이미지에 그대로 옮겨 2020년 컴퓨터 비전의 판도를 바꾼 Vision Transformer(ViT)를 다룬다. 다만 발상이 단순한 만큼 대가가 분명해서, ViT를 실제로 쓰는 일은 대부분 「무엇을 포기했고 그것을 어디서 되찾을 것인가」를 고르는 일이 된다.

패치 시퀀스

Vision Transformer 아키텍처

패치 분할과 선형 임베딩

ViT는 이미지를 정사각 조각으로 자르고 조각마다 벡터 하나를 만든다. 224×224 이미지를 16×16 패치로 나누면 14×14, 곧 196개의 조각이 나오고 각 조각은 16×16×3=76816 \times 16 \times 3 = 768 개의 숫자다. 이것을 선형 레이어 하나로 768차원 벡터에 옮기면 문장의 단어 임베딩과 형태가 완전히 같은 시퀀스가 된다. 그 뒤로는 NLP의 Transformer 인코더를 글자 그대로 가져다 쓴다.

구현에서는 자르기와 선형 변환을 따로 하지 않고 합성곱 하나로 처리한다. 커널 크기와 스트라이드를 둘 다 패치 크기로 두면 서로 겹치지 않는 조각마다 같은 선형 변환이 한 번씩 걸리므로, 정의상 패치 분할과 임베딩을 함께 한 것이 된다.

여기서 한 가지가 조용히 버려진다는 점을 짚어 둘 만하다. 조각 안의 16×16 배치는 선형 변환이 위치별로 다른 가중치를 쓰므로 그대로 살아 있지만, 조각과 조각 사이의 공간 관계는 이 단계에서 전부 사라진다. 196개 벡터는 그냥 목록이고 어느 것이 어느 것의 위쪽이었는지에 대한 정보가 없다. 뒤에 볼 위치 임베딩이 그 자리를 메우는 유일한 장치이며, 그것을 빼고 학습시키면 이미지를 무작위로 섞어 넣은 것과 같은 결과가 나온다.

import torch.nn as nn

# 커널=스트라이드=16 → 겹치지 않는 패치마다 같은 선형 변환
proj = nn.Conv2d(3, 768, kernel_size=16, stride=16)
# (B, 3, 224, 224) → (B, 768, 14, 14) → flatten → (B, 196, 768)

패치 크기가 정하는 비용

패치 크기는 ViT에서 가장 먼저 정해야 하는 값이고, 정확도와 비용을 동시에 움직이는 유일한 손잡이다. 자기 어텐션의 비용은 토큰 수의 제곱에 비례하는데, 패치를 절반으로 줄이면 토큰 수가 네 배가 되므로 어텐션 비용은 열여섯 배가 된다.

패치 토큰 수 어텐션 쌍 16×16 대비
16×16 196 38,416 1배
14×14 256 65,536 1.7배
8×8 784 614,656 16배

작은 패치가 정확도에서 유리한 이유는 단순하다. 조각 하나가 곧 모델이 볼 수 있는 가장 작은 단위라서, 16픽셀보다 작은 무늬는 임베딩 단계에서 이미 뭉개진 채 들어간다. 그래서 세밀한 질감이 중요한 과제일수록 14나 8이 이기고, 대신 그 대가를 어텐션 비용으로 낸다. ViT-L/16과 ViT-L/14가 같은 파라미터 수로 다른 속도를 갖는 이유가 여기 있다 — 패치 크기는 파라미터를 거의 늘리지 않고 연산량만 늘린다.

CLS 토큰

패치 196개를 넣으면 나오는 것도 196개다. 분류에는 이미지 전체를 대표하는 벡터 하나가 필요한데, ViT는 BERT를 따라 CLS 토큰을 맨 앞에 하나 더 붙인다. 이 토큰은 픽셀에서 오지 않고 학습되는 파라미터이며, 어텐션을 거치며 모든 패치의 정보를 끌어모아 마지막 층에서 분류기의 입력이 된다.

대안으로 마지막 층의 패치 토큰 전체를 평균 내는 방식도 쓰이고, 실제로 성능 차이는 거의 없다. 다만 학습률을 같은 값으로 두면 둘의 최적 구간이 다르다는 것이 알려져 있어서, 풀링 방식을 바꿀 때는 학습률도 함께 다시 잡아야 한다. 분할·검출처럼 위치가 필요한 과제에서는 CLS를 버리고 패치 토큰을 그대로 특징 맵으로 되돌려 쓴다.

귀납 편향

합성곱이 공짜로 갖는 것

귀납 편향은 모델 구조가 미리 품고 있는 가정이다. 합성곱은 둘을 공짜로 갖는다. 하나는 지역성이다 — 커널이 이웃 픽셀만 보므로 「가까운 픽셀끼리 관계가 깊다」는 가정이 구조에 박혀 있다. 다른 하나는 평행이동 등변성이다. 같은 커널이 모든 위치를 훑으므로 고양이가 왼쪽에 있든 오른쪽에 있든 같은 특징이 같은 방식으로 잡힌다.

ViT의 어텐션에는 이 둘이 없다. 첫 층부터 모든 패치가 모든 패치를 볼 수 있어서 이웃이라는 개념 자체가 구조에 없고, 위치는 더해 주는 임베딩으로만 알려 준다. 데이터에서 직접 배워야 한다는 뜻이다.

데이터 양과 역전

배워야 한다는 말은 데이터가 그만큼 필요하다는 말이다. 원 논문이 세 규모에서 같은 실험을 돌려 보인 것이 정확히 이 곡선이다. ImageNet-1k의 128만 장만 쓰면 같은 크기의 ResNet이 ViT를 이기고, ImageNet-21k의 1,400만 장에서 둘이 비슷해지고, JFT-300M의 3억 장에서 ViT가 앞선다. 모델이 나빠서 지는 것이 아니라 공짜로 받을 가정을 데이터로 사는 중이라 데이터가 모자라면 값을 못 치르는 것이다.

그래서 「ViT가 CNN보다 좋은가」는 질문 자체가 성립하지 않는다. 물어야 할 것은 내가 가진 데이터가 그 역전 지점의 어느 쪽에 있는가이고, 대부분의 실무 데이터는 왼쪽에 있다. 사전학습 가중치를 받아 쓰는 관행이 보편화된 진짜 이유도 이것이다 — 남이 3억 장으로 치른 값을 넘겨받는 것이다.

역전 지점을 몇 장이라고 못 박아 외우는 것은 위험하다. 그 값은 데이터 개수만이 아니라 과제의 성격과 증강 설정에 함께 달려 있어서, 같은 10만 장이라도 범주가 단순하고 배경이 고른 데이터에서는 ViT가 일찍 따라잡고 범주가 미세하게 갈리는 데이터에서는 한참 뒤처진다. 실제로 확인하는 방법은 하나뿐이다 — 같은 예산으로 CNN 기준선을 먼저 세워 두고 그 위에서 ViT의 이득을 재는 것이다. 기준선 없이 ViT부터 돌리면 나온 숫자가 좋은 것인지 나쁜 것인지를 판단할 자리가 없다.

편향을 되돌려 넣는 길

이후 연구는 대부분 「잃은 편향을 어디서 되찾을 것인가」로 갈린다. 세 갈래가 있다. 구조로 되찾는 쪽은 어텐션 범위를 이웃으로 제한하거나 합성곱 층을 앞단에 섞는다. 학습으로 되찾는 쪽은 CNN 교사에게 배우거나 증강을 극단으로 올린다. 데이터로 되찾는 쪽은 라벨 없는 이미지로 자기지도 사전학습을 한다. 뒤의 두 절이 각각 두 번째와 세 번째 길이고, Swin이 첫 번째 길이다.

위치 임베딩

학습형 1D 임베딩

ViT의 위치 정보는 토큰 개수만큼의 벡터를 파라미터로 두고 패치 임베딩에 더하는 방식이다. 197개 자리에 각각 768차원 벡터 하나씩, 그게 전부다. 2차원 좌표를 따로 인코딩하지 않고 1차원 번호만 주는데도 잘 동작하는 이유는, 학습을 마친 임베딩끼리 유사도를 재 보면 같은 행·같은 열에 있는 자리끼리 높게 나오기 때문이다. 격자 구조를 데이터에서 스스로 복원한 셈이다.

해상도를 바꿀 때의 보간

문제는 이 방식이 토큰 개수에 못 박혀 있다는 점이다. 224로 학습한 모델에 384 이미지를 넣으면 패치가 576개(24×24)가 되는데 위치 임베딩은 196개뿐이다. 자리마다 배울 벡터가 없으니 새로 학습하지 않고는 쓸 수 없어 보이지만, 앞 문단의 사실이 길을 열어 준다 — 임베딩이 이미 격자를 담고 있으므로 14×14 격자로 되돌린 뒤 24×24로 늘리면 된다.

절차는 셋이다. CLS 자리의 벡터를 떼어 두고, 나머지 196개를 14×14×768 격자로 reshape해 바이큐빅으로 24×24까지 보간하고, 다시 펴서 CLS를 앞에 붙인다. 고해상도 파인튜닝이 사실상 표준 레시피가 된 것은 이 보간이 잘 듣기 때문이다.

import torch.nn.functional as F

cls, grid = pos_embed[:, :1], pos_embed[:, 1:]
grid = grid.reshape(1, 14, 14, 768).permute(0, 3, 1, 2)
grid = F.interpolate(grid, size=(24, 24), mode="bicubic", align_corners=False)
grid = grid.permute(0, 2, 3, 1).reshape(1, 576, 768)
pos_embed = torch.cat([cls, grid], dim=1)

상대 위치

보간이 통한다고 해서 절대 위치 임베딩이 최선인 것은 아니다. 두 패치 사이의 관계에 정말 중요한 것은 각자의 절대 좌표가 아니라 둘의 상대적 거리인 경우가 많다. 그래서 Swin을 비롯한 이후 모델은 어텐션 점수에 두 토큰의 상대 좌표에 따른 편향값을 직접 더한다. 해상도가 바뀌어도 상대 거리의 의미는 그대로라 보간이 훨씬 자연스럽고, 창 안에서만 어텐션을 계산할 때는 필요한 편향표도 창 크기만큼으로 작아진다.

인코더 블록

Pre-LN

ViT의 인코더 블록은 NLP Transformer와 거의 같지만 정규화 위치가 다르다. 원 Transformer는 어텐션과 MLP를 지난 뒤에 LayerNorm을 걸었고, ViT는 들어가기 전에 건다. 이 차이가 중요한 것은 깊은 모델의 학습 안정성 때문이다. Pre-LN에서는 잔차 경로가 정규화를 거치지 않고 입력에서 출력까지 곧장 이어지므로, 층을 스물넷까지 쌓아도 초기 학습률 워밍업 없이 발산하지 않는다.

def forward(self, x):
    normed = self.norm1(x)
    x = x + self.attn(normed, normed, normed)[0]
    x = x + self.mlp(self.norm2(x))
    return x

MLP 확장비

각 블록의 MLP는 차원을 네 배로 부풀렸다가 되돌린다. ViT-B라면 768 → 3072 → 768이고 활성화는 ReLU가 아니라 GELU다. 파라미터의 무게중심이 여기 있다는 점을 알아 둘 만하다. 블록 하나에서 어텐션이 쓰는 파라미터는 4d24d^2 인데 MLP는 8d28d^2 라서, 자기 어텐션이 ViT의 특징으로 불리는 것과 달리 파라미터의 3분의 2는 MLP에 있다.

크기 표기

ViT 계열의 이름은 두 조각으로 읽는다. 앞의 문자가 모델 폭과 깊이, 뒤의 숫자가 패치 크기다.

이름 층 차원 헤드 파라미터
ViT-B/16 12 768 12 86M
ViT-L/16 24 1024 16 307M
ViT-H/14 32 1280 16 632M

같은 B라도 /16과 /8은 파라미터가 거의 같고 속도만 크게 다르다. 반대로 B와 L은 파라미터가 3.5배 차이 난다. 모델을 고를 때 이 둘을 섞어 생각하면 예산 계산이 어긋난다 — 가중치 파일 크기와 GPU 메모리는 앞 문자가 정하고, 초당 처리 장수는 뒤 숫자가 크게 흔든다. 그래서 메모리가 빠듯하면 B를 유지한 채 패치를 키우는 쪽이 아니라 해상도를 내리는 쪽을 먼저 본다. 패치를 키우면 그만큼 작은 무늬가 임베딩에서 사라지는데, 해상도를 내리는 것은 적어도 무엇을 잃는지가 눈에 보인다.

데이터 효율 학습

ViT 계열 모델 비교

DeiT의 증류 토큰

3억 장을 못 구하는 쪽이 대부분이라, ImageNet-1k만으로 ViT를 쓸 만하게 만드는 일이 곧바로 과제가 됐다. DeiT의 답은 CNN 교사에게 배우는 것이다. CLS 토큰 옆에 증류 토큰을 하나 더 붙이고, CLS는 정답 라벨을, 증류 토큰은 교사 CNN의 예측을 맞히도록 따로 학습시킨다. 교사가 가진 지역성·등변성 가정이 그 예측에 묻어 있으므로, ViT는 그것을 흉내 내면서 잃었던 편향을 우회로로 얻는다.

교사를 CNN으로 두는 것이 핵심이고, 같은 실험을 Transformer 교사로 바꾸면 이득이 줄어든다. 여기에 RandAugment·Mixup·CutMix 같은 강한 증강과 수백 에폭의 긴 학습이 함께 붙는다 — 데이터가 적을 때 ViT를 살리는 것은 한 가지 기법이 아니라 이 조합 전체다.

MAE의 마스킹

다른 길은 라벨을 아예 쓰지 않는 것이다. MAE는 패치의 75%를 무작위로 가리고 남은 25%만 인코더에 넣은 뒤, 가벼운 디코더가 가려진 자리의 픽셀을 복원하게 한다. 비율이 이렇게 높은 데는 이유가 있다. 이미지는 언어보다 훨씬 중복이 심해서 15%만 가리면 바로 옆 픽셀을 베껴 채울 수 있고, 그러면 모델이 의미를 배우지 않고 보간만 배운다. 75%를 가려야 비로소 가려진 자리를 채우는 일이 「이게 무엇인지」를 알아야 풀리는 문제가 된다.

부수 효과로 학습이 빨라진다. 인코더가 전체가 아니라 4분의 1의 토큰만 보므로 한 에폭 비용이 크게 줄고, 어텐션 비용은 토큰 수의 제곱이라 감소 폭이 더 크다. 라벨 없는 이미지가 쌓여 있는 도메인이라면 이쪽이 증류보다 먼저 시도할 길이다.

사전학습을 고르는 순서

받아 쓸 가중치를 고르는 기준은 보통 셋이다. 내 데이터가 자연 이미지에 가까우면 ImageNet-21k 지도학습 가중치가 무난하고, 도메인이 멀면(의료 영상·위성 사진) 자기지도 가중치가 낫다. 분류만 필요하면 ViT를, 검출·분할이 섞이면 뒤에 볼 계층형 백본을 고른다. 그리고 라벨 없는 자체 데이터가 수만 장 있다면 받아 온 가중치 위에 그 데이터로 자기지도 학습을 한 번 더 얹는 선택지가 있다.

Swin

Swin의 창 어텐션과 이동 창

창 어텐션

ViT를 검출·분할에 쓰려면 벽이 둘 있다. 어텐션 비용이 토큰 수의 제곱이라 고해상도 입력이 감당이 안 되고, 모든 층의 해상도가 같아서 CNN처럼 여러 크기의 특징 맵을 내놓지 못한다. Swin은 어텐션을 창 안으로 가둬 첫 번째 벽을 넘는다. 특징 맵을 7×7 패치짜리 창으로 나누고 어텐션을 창 안에서만 계산하면, 비용이 토큰 수의 제곱이 아니라 토큰 수에 비례하게 된다.

차이는 고해상도에서 압도적이다. 56×56 격자의 토큰 3,136개를 전역으로 보면 어텐션 쌍이 983만 개인데, 49개짜리 창 64개로 나누면 창마다 2,401쌍씩 15만 쌍이다. 같은 입력에 64분의 1이다. 더 중요한 것은 이 비율이 입력이 커질수록 벌어진다는 점이다. 창 크기를 7로 고정해 두면 해상도를 두 배로 올려도 창 개수만 네 배가 되므로 비용이 네 배로 그치는데, 전역 어텐션은 같은 조건에서 열여섯 배가 된다. 검출·분할처럼 입력을 키워야 하는 과제에서 이 차이가 곧 쓸 수 있느냐 없느냐를 가른다.

이동 창

창으로 가두면 창 경계를 넘는 관계가 끊긴다. 왼쪽 창의 물체와 오른쪽 창의 물체는 영영 서로를 못 본다. Swin은 층마다 창 격자를 절반 크기만큼 대각선으로 밀어 이 문제를 푼다. 한 층에서 같은 창에 없던 두 패치가 다음 층에서는 같은 창에 들어오므로, 두 층을 한 묶음으로 보면 정보가 창 경계를 건너간다. 전역 어텐션을 한 번에 하는 대신 여러 층에 걸쳐 조금씩 퍼뜨리는 방식이다.

밀고 나면 가장자리에 창 크기에 못 미치는 조각이 생기는데, 그 조각들을 따로 처리하면 창 크기가 제각각이 되어 배치 계산이 깨진다. 그래서 구현은 특징 맵을 원환처럼 굴려 반대편 가장자리를 붙여 놓고, 원래 떨어져 있던 영역끼리는 어텐션이 걸리지 않도록 마스크를 씌운다. 창 개수와 크기를 그대로 유지하면서 경계만 건너뛰게 하는 방법이다.

계층 특징 맵

두 번째 벽은 병합으로 넘는다. 단계가 올라갈 때마다 이웃한 2×2 패치를 하나로 합치고 채널을 두 배로 늘린다. 해상도가 절반씩 줄고 채널이 배로 느는 이 형태는 ResNet의 단계 구성과 정확히 같아서, 검출·분할 프레임워크가 기대하는 다중 스케일 특징 맵을 그대로 내놓는다. Swin이 발표되자마자 검출 백본으로 널리 쓰인 것은 정확도 때문만이 아니라 기존 파이프라인에 갈아 끼우기만 하면 됐기 때문이다.

실전 선택

파인튜닝 레시피

사전학습 ViT를 내 데이터에 맞추는 데는 관행이 된 설정이 있다. 첫째는 층별 학습률 감쇠다. 출력에 가까운 층일수록 큰 학습률을, 입력에 가까운 층일수록 작은 학습률을 주는데, 층마다 0.65~0.75를 곱해 내려가는 식이다. 아래층이 배운 일반적인 특징을 적은 데이터로 망가뜨리지 않으려는 장치다.

둘째는 해상도 상향이다. 224로 파인튜닝한 뒤 384로 한 번 더 짧게 돌리면 대개 정확도가 오른다. 앞에서 본 위치 임베딩 보간이 이때 필요하다. 셋째는 데이터가 적을 때의 정규화다. 드롭아웃보다 stochastic depth(층을 확률적으로 건너뛰기)가 잘 듣고, Mixup·CutMix를 함께 건다. 사전학습 가중치를 얼려 두고 마지막 층만 학습하는 선형 프로빙은 결과 자체보다 진단용으로 쓴다 — 선형 프로빙 점수가 낮으면 그 가중치가 내 도메인과 맞지 않는다는 뜻이라 파인튜닝 설정을 아무리 만져도 한계가 있다.

어텐션 맵 읽기

ViT는 자기가 어디를 봤는지 어텐션 가중치로 보여 준다. 다만 마지막 층의 가중치만 그려 보면 대개 읽을 수 없는 얼룩이 나온다. 층을 지날수록 잔차 연결로 정보가 섞이기 때문에, 층마다의 어텐션 행렬에 항등 행렬을 더하고 정규화한 뒤 층 순서대로 곱해 입력까지 되짚는 방식을 쓴다.

그렇게 보면 CNN과 다른 점이 하나 드러난다. CNN의 얕은 층은 언제나 좁은 범위만 보는 반면, ViT는 첫 층부터 이미지 전역을 보는 헤드와 바로 옆만 보는 헤드가 함께 있다. 좁게 보는 헤드가 사실상 합성곱이 하던 일을 스스로 배운 것이라, 앞에서 말한 「데이터로 편향을 산다」가 실제로 어떤 모양인지 눈으로 확인되는 자리다.

CNN과 ViT 고르기

상황 고를 것
라벨 데이터 1만 장 미만 CNN 또는 사전학습 ViT의 선형 프로빙
라벨 데이터 10만 장 이상 ViT·DeiT 파인튜닝
검출·분할 Swin 등 계층형 백본
모바일·엣지 경량 CNN 계열
텍스트와 함께 쓰는 비전 인코더 ViT 계열

표의 왼쪽 열이 전부 내 데이터와 내 과제라는 점이 중요하다. 실무에서 순수 ViT를 처음부터 학습하는 일은 거의 없고, 고르는 대상은 사전학습 가중치와 그 위에 얹을 레시피다.

다음 글에서는 이 ViT를 비전 인코더로 쓰되 라벨 대신 이미지와 문장의 짝으로 학습해, 본 적 없는 범주까지 분류하는 CLIP의 대조 학습을 다룬다.


읽어주셔서 감사합니다. 😊

LATEST

비전·음성·추천의 최신 글

비전·음성·추천2026.09.07

오프라인 강화학습 — 쌓인 로그만으로 정책을 배우기

실제 서비스에서 탐색은 곧 사용자에게 나쁜 행동을 해 보는 일입니다. 이미 쌓인 로그만으로 정책을 배우려 할 때 왜 Q값이 혼자 부풀어 오르는지, 그 부풀음을 누르는 세 갈래 대응, 행동 복제라는 기준선의 무게, 그리고 배포 전에 성능을 재는 일이 왜 가장 어려운지를 정리합니다.

18 MIN
비전·음성·추천2026.09.07

다국어 전이 — 라벨 없는 언어에서 모델이 동작하는 이유

영어 라벨만으로 학습한 분류기가 한국어 문장을 그대로 처리하는 일이 실제로 일어납니다. 여러 언어가 한 표현 공간에 겹쳐 놓이는 원리, 그 겹침이 무너지는 조건, 번역해서 학습할지 번역해서 추론할지 고르는 기준, 그리고 언어별로 나눠 재야 하는 이유를 정리합니다.

16 MIN
비전·음성·추천2026.09.07

정보 추출 — 글 한 덩이를 표 한 줄로 바꾸는 일

계약서와 이메일을 데이터베이스에 넣으려면 글에서 값을 뽑아 칸에 채워야 합니다. 개체명·관계·사건의 세 층위, 값을 정규화하는 일이 왜 절반인지, 근거 위치를 함께 남겨야 하는 이유, 규칙·전용 모델·언어 모델의 갈림길, 그리고 필드별로 재는 평가법을 정리합니다.

17 MIN