비전 트랜스포머
Part 1에서 우리는 합성곱 신경망(CNN)이 어떻게 이미지의 국소적 특징을 학습하는지를 살펴보았습니다. 이 장은 그 위에서 한 단계 더 나아갑니다. 먼저 CNN이 영상 인식의 표준으로 자리 잡기까지의 흐름을 짚고, 이어서 합성곱이라는 틀 자체를 벗어나 이미지를 "패치의 시퀀스"로 다루는 트랜스포머 계열 모델로 넘어갑니다.
트랜스포머는 본래 자연어 처리를 위해 고안된 구조였지만, 이미지에도 그대로 적용할 수 있다는 것이 밝혀지면서 컴퓨터 비전의 지형을 바꾸어 놓았습니다. 이 장에서는 그 핵심 아이디어를 코드로 직접 확인합니다.
1깊이를 향한 경쟁: VGG-16에서 ResNet까지¶
2012년 AlexNet이 1998년의 LeNet을 재발견하여 합성곱 계층을 다시 무대 위로 끌어올린 이후, 이미지넷 경진대회의 우승권 모델들은 한 가지 공통점을 공유합니다. 모두 합성곱 신경망을 기반으로 한다는 것입니다. 이 시기의 발전은 "어떻게 하면 더 깊게 쌓을 수 있는가"라는 질문을 중심으로 전개되었습니다.
2014년 VGG-16은 AlexNet과 근본적으로 같은 기법을 쓰되, 성능을 끌어올리기 위해 신경망을 훨씬 더 깊게 만들었습니다. LeNet이 합성곱 계층을 두 개, AlexNet이 다섯 개 사용한 것과 달리 VGG-16은 합성곱 계층을 13개 쓰고 총 16개 계층으로 구성됩니다. 새로운 유형의 기술을 제시한 것은 아니지만, 합성곱과 풀링의 조합만으로도 깊이가 곧 성능이 될 수 있음을 본격적으로 보여준 모델입니다.
2014년 GoogLeNet은 같은 해 우승을 차지했습니다. 이름에서 드러나듯 LeNet의 계보를 잇지만, 계층이 신호를 순차적으로만 전달하던 방식에서 벗어났습니다. 하나의 신호를 여러 구성의 합성곱 계층에 병렬적으로 흘려보내 동시에 처리한 뒤 다시 취합하는 인셉션(Inception) 구조를 도입한 것입니다. 합성곱과 풀링을 순차적으로만 거듭하면 신호의 크기가 점점 줄어들어 — 뜨거운 물에 각설탕이 녹듯 — 결국 합성곱의 이점을 살리기 어려울 만큼 작아집니다. 병렬 처리는 이 깊이의 한계를 우회하는 한 가지 해법이었습니다.
흥미로운 점은 VGG-16과 GoogLeNet의 오류율이 각각 7.3%, 6.7%로 그 차이가 1% 미만이었다는 사실입니다. GoogLeNet은 훨씬 깊고 복잡해 고도의 인프라가 없으면 훈련이 어렵지만, VGG-16은 순차적이고 비교적 얕아 준수한 워크스테이션에서도 훈련할 수 있습니다. 그래서 VGG-16은 준우승에 그쳤음에도 간결한 구성 덕분에 이후 수많은 응용에서 널리 쓰였습니다.
2015년 ResNet은 마이크로소프트가 개발해 ILSVRC에서 우승했고, Top-5 오류율 3.5%로 보통의 성인 수준을 뛰어넘는 인식 성능에 도달했습니다. 천 가지 유형, 백만 장 규모의 이미지넷이 지향하던 "인간 수준 인식"이라는 목표가 달성된 순간입니다.
ResNet의 핵심은 입력을 합성곱 계층 하나씩 건너뛰어 그대로 전달하는 **스킵 연결(skip connection)**입니다. 층이 깊어질수록 역전파 과정에서 기울기가 점점 작아지고 왜곡되는 기울기 소실 문제가 생기는데, 스킵 연결은 신호가 감쇠하는 경로를 우회시켜 깊은 모델도 안정적으로 학습되게 합니다. 그 결과 ResNet은 34개 계층에 이르면서도 대체로 순차적인 구조를 유지합니다.
ResNet 이후 영상 데이터에 대한 접근은 합성곱 신경망을 기본으로 삼게 되었고, 사전훈련된 가중치를 다른 목표에 재활용하는 전이학습의 형태로 폭넓게 활용되었습니다.
| 연도 | 모델 | 깊이 | 핵심 아이디어 | Top-5 오류율 |
|---|---|---|---|---|
| 1998 | LeNet | 합성곱 2 | 합성곱 도입 | — |
| 2012 | AlexNet | 합성곱 5 | 합성곱의 재발견 | — |
| 2014 | VGG-16 | 16 | 깊이 = 성능 | 7.3% |
| 2014 | GoogLeNet | 깊음 | 인셉션(병렬) | 6.7% |
| 2015 | ResNet | 34+ | 스킵 연결 | 3.5% |
2합성곱을 넘어서: 트랜스포머¶
VGG에서 ResNet까지의 흐름은 모두 "합성곱을 어떻게 더 깊게, 더 안정적으로 쌓을 것인가"라는 질문의 변주였습니다. 합성곱은 본질적으로 국소적입니다. 작은 커널이 인접한 화소들을 묶어 특징을 만들고, 그것을 거듭 쌓아 점점 더 넓은 영역을 보게 됩니다. 이미지의 멀리 떨어진 두 영역 사이의 관계를 포착하려면 그만큼 깊이 쌓아야 합니다.
트랜스포머는 전혀 다른 전제에서 출발합니다. 다음 장의 Vision Transformer가 그 출발점입니다.
3Vision Transformer¶
트랜스포머는 원래 자연어 처리에 사용되던 모델이지만, 최근에는 컴퓨터 비전 분야에서도 활용되고 있습니다. Vision Transformer (ViT)는 트랜스포머 아키텍처를 이미지 인식에 적용한 모델입니다. ViT는 이미지를 작은 패치로 나누고, 각 패치를 시퀀스로 처리하여 이미지의 특징을 학습합니다. ViT는 기존의 합성곱 신경망 (CNN)보다 더 긴 범위의 의존성을 캡처할 수 있어, 이미지 인식에서 뛰어난 성능을 보여줍니다.
여기서 결정적인 발상은 이미지를 작은 패치들의 시퀀스로 바꾼다는 점입니다. 문장이 단어의 나열이듯, 이미지를 패치의 나열로 보는 것입니다. 패치들을 한꺼번에 트랜스포머에 넣으면, 멀리 떨어진 패치끼리도 한 단계 안에서 직접 관계를 맺을 수 있습니다. 합성곱이 여러 층을 거쳐야 닿을 수 있던 긴 범위의 의존성을 ViT는 처음부터 다룰 수 있다는 뜻입니다.
4ViT 기반 모델 체험: Segment Anything¶
개념을 코드로 만나보기 위해, 트랜스포머 기반 비전 모델 중 하나인 SAM(Segment Anything Model)을 직접 불러와 봅니다. SAM은 이미지를 인코딩하는 백본으로 ViT 구조를 사용하며, 별도의 학습 없이도 이미지 안의 임의의 객체를 분할(segmentation)해 냅니다.
from ultralytics import SAM # Segment Anything Model (SAM)
model = SAM('sam_b.pt')print(f'매개변수: {sum(p.numel() for p in model.parameters()):,}')
print(model)매개변수의 개수와 모델 구조를 출력해 보면, 이 모델이 합성곱 블록의 단순한 반복이 아니라 패치 임베딩과 어텐션 계층으로 짜인 트랜스포머 백본을 품고 있음을 확인할 수 있습니다. 이제 실제 이미지에 적용해 결과를 살펴봅시다.
파일경로 = 'data/coco128/images/train2017/000000000074.jpg'
results = model(파일경로)results[0].show()5패치 임베딩의 구조¶
ViT의 동작을 가장 압축적으로 요약하면 다음 한 줄이 됩니다.
핵심 구상: 이미지는 16x16의 패치, 패치마다 768차원의 벡터로 표현, 트랜스포머로 처리
즉 이미지를 16×16 크기의 패치로 잘게 나누고, 각 패치를 768차원의 벡터로 표현한 다음, 그 벡터들의 시퀀스를 트랜스포머로 처리합니다. 여기서 “패치를 잘라 벡터로 바꾸는” 첫 단계가 바로 **패치 임베딩(patch embedding)**입니다.
패치 임베딩은 새로운 연산이 아니라, 우리가 이미 아는 합성곱 한 번으로 깔끔하게 구현됩니다. 커널 크기와 보폭(stride)을 모두 패치 크기와 똑같이 16으로 맞춘 합성곱을 생각해 봅시다. 이렇게 하면 커널이 겹침 없이 16칸씩 건너뛰며 움직이므로, 각 위치에서 정확히 하나의 16×16 패치를 보고 768개의 출력값(필터 수)을 만들어 냅니다. 결과적으로 "패치 하나 → 768차원 벡터"라는 임베딩이 그대로 얻어집니다.
아래 코드에서 입력은 이미지입니다. 패치 크기가 16이므로 한 변은 개의 패치로 나뉘고, 전체는 개의 패치가 됩니다. 따라서 출력의 공간 격자는 , 채널은 임베딩 차원인 768이 됩니다.
import numpy as np
import keras
from keras import layers
import PIL.Image as Image
model = keras.Sequential([
layers.Input(shape=(160, 160, 3)),
# 패치 임베딩
layers.Conv2D(768, kernel_size=16, strides=16), # 패치 크기: 16x16, 임베딩 차원: 768
])
with Image.open('data/mozzi.png') as img:
img = img.convert('RGB').resize((160, 160))
sample = np.array(img) / 255.0 # 정규화
outputs = model.predict(np.array([sample]))
print(outputs.shape)출력 형태는 (1, 10, 10, 768)입니다. 이 격자를 한 줄로 펴면 길이 100짜리 시퀀스가 되고, 각 원소는 768차원 벡터, 즉 하나의 패치 토큰이 됩니다. 문장이 100개의 단어로 이루어진 것과 똑같은 형태가 된 셈입니다.
이 토큰 시퀀스가 이후 트랜스포머로 들어가면, 어텐션을 통해 패치들이 서로의 정보를 직접 주고받습니다. 합성곱이 깊이를 쌓아 국소에서 전역으로 시야를 넓혀갔다면, ViT는 패치 임베딩이라는 한 번의 변환만으로 곧장 전역적인 관계를 다룰 무대를 마련합니다. VGG에서 ResNet으로 이어진 "깊이의 경쟁"이 합성곱이라는 틀 안에서의 진화였다면, 비전 트랜스포머는 그 틀 자체를 다시 묻는 전환이라 할 수 있습니다.