🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Vision Transformer (ViT) 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 9. · 12 Views

Vision Transformer (ViT) 완벽 가이드

CNN의 한계를 넘어선 이미지 인식의 새로운 패러다임, Vision Transformer를 초급 개발자 눈높이에서 풀어냅니다. 이미지 패치 분할부터 Self-Attention까지, 실무 예제와 함께 술술 읽히는 설명으로 ViT의 모든 것을 배웁니다.


목차

  1. 이미지 패치 분할
  2. Patch Embedding
  3. Position Embedding
  4. Self-Attention in Vision
  5. ViT 아키텍처 구조
  6. CNN vs ViT 비교

1. 이미지 패치 분할

어느 날 김개발 씨가 이미지 분류 모델을 개선하는 업무를 맡았습니다. 선배 박시니어 씨가 "이번엔 Vision Transformer를 한번 써보는 게 어때요?"라고 제안했습니다.

김개발 씨는 고개를 갸우뚱했습니다. "Transformer가 자연어 처리용 아니었나요?"

이미지 패치 분할은 하나의 큰 이미지를 여러 개의 작은 사각형 조각으로 나누는 과정입니다. 마치 퍼즐 조각처럼 이미지를 일정한 크기로 잘라내는 것입니다.

이렇게 나눈 패치들을 Transformer가 처리할 수 있는 시퀀스 데이터로 변환합니다. Transformer는 원래 단어 시퀀스를 처리하는데, 이미지 패치를 단어처럼 취급하는 것이 핵심 아이디어입니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

# 224x224 이미지를 16x16 패치로 분할
def split_image_to_patches(image, patch_size=16):
    # image: (batch, channels, height, width)
    # 예: (1, 3, 224, 224)
    batch_size, channels, height, width = image.shape

    # 패치 개수 계산: 224 / 16 = 14개씩
    num_patches = (height // patch_size) * (width // patch_size)

    # unfold를 사용해 패치 추출
    # 결과: (batch, channels, num_patches_h, num_patches_w, patch_size, patch_size)
    patches = image.unfold(2, patch_size, patch_size).unfold(3, patch_size, patch_size)

    # 형태 변환: (batch, num_patches, channels * patch_size * patch_size)
    patches = patches.contiguous().view(batch_size, num_patches, -1)

    return patches  # (1, 196, 768) - 196개의 패치, 각 768차원

김개발 씨는 회의실에서 박시니어 씨의 설명을 듣고 있었습니다. "이미지를 Transformer로 처리한다고요?

어떻게요?" 박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다. "자, 여기 224x224 크기의 강아지 사진이 있다고 해봅시다.

이 이미지를 그냥 Transformer에 넣을 수 있을까요?" 김개발 씨는 고개를 저었습니다. "Transformer는 단어 시퀀스를 입력받는데, 이미지는 픽셀 배열이잖아요." 정확히 그 문제를 해결하기 위해 이미지 패치 분할이라는 개념이 등장했습니다.

쉽게 비유하자면, 이미지 패치 분할은 마치 신문을 여러 개의 작은 조각으로 자르는 것과 같습니다. 신문 전체를 한 번에 읽기보다는, 각 단락을 따로 떼어내서 순서대로 읽는 것입니다.

이렇게 하면 긴 신문 기사도 여러 개의 짧은 문단으로 나뉘어 처리하기 쉬워집니다. 이미지 패치 분할도 마찬가지로 큰 이미지를 작은 조각들로 나누어 Transformer가 처리할 수 있게 만듭니다.

이미지 패치 분할이 없던 시절에는 어땠을까요? 초기 연구자들은 이미지의 모든 픽셀을 하나씩 Transformer에 입력하려고 시도했습니다.

224x224 이미지라면 50,176개의 픽셀이 있고, 이것이 모두 개별 토큰이 됩니다. 문제는 Transformer의 Self-Attention 메커니즘이 토큰 개수의 제곱에 비례하는 계산량을 가진다는 것입니다.

50,176개 토큰이라면 계산량이 천문학적으로 커집니다. 메모리도 터지고, 학습 시간도 며칠씩 걸렸습니다.

바로 이런 문제를 해결하기 위해 이미지 패치 분할이 등장했습니다. 패치 분할을 사용하면 토큰 개수를 획기적으로 줄일 수 있습니다.

16x16 크기로 패치를 나누면 224x224 이미지는 14x14 = 196개의 패치로 변환됩니다. 토큰 개수가 50,176개에서 196개로 줄어드는 것입니다.

또한 각 패치가 국소적인 이미지 정보를 담고 있어, CNN의 장점도 일부 활용할 수 있습니다. 무엇보다 계산 효율성이라는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 5번째 줄에서 이미지의 배치 크기, 채널 수, 높이, 너비를 추출합니다.

PyTorch에서 이미지는 보통 (배치, 채널, 높이, 너비) 순서로 저장됩니다. 8번째 줄에서는 패치 개수를 계산합니다.

224를 16으로 나누면 한 방향으로 14개씩, 총 196개의 패치가 나옵니다. 12번째 줄의 unfold 함수가 핵심입니다.

이 함수는 이미지를 슬라이딩 윈도우 방식으로 잘라내는데, patch_size 크기의 윈도우를 patch_size만큼 이동하면서 겹치지 않게 패치를 추출합니다. 마지막으로 15번째 줄에서 추출된 패치들을 (배치, 패치 개수, 패치 차원) 형태로 재배열합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 의료 영상 분석 서비스를 개발한다고 가정해봅시다.

X-ray 이미지에서 폐렴을 진단하는 모델을 만들 때, 패치 분할을 활용하면 이미지의 각 영역을 독립적으로 분석할 수 있습니다. 폐의 왼쪽 부분, 오른쪽 부분, 중앙 부분 등을 패치로 나누어 각각의 특징을 학습합니다.

구글, 메타 같은 빅테크 기업들이 이미지 검색, 자율주행 등에서 이런 패턴을 적극적으로 사용하고 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 패치 크기를 너무 작게 설정하는 것입니다. 패치를 4x4로 설정하면 패치 개수가 너무 많아져서 계산량이 폭증합니다.

이렇게 하면 학습 속도가 느려지고 메모리 부족 오류가 발생할 수 있습니다. 따라서 16x16 또는 32x32가 일반적으로 권장되는 패치 크기입니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 이미지를 단어처럼 만드는 거군요!" 패치 분할을 제대로 이해하면 ViT의 나머지 구조도 쉽게 이해할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 일반적으로 16x16 패치 크기가 성능과 속도의 균형점입니다

  • 패치 크기가 클수록 계산량은 줄지만, 세밀한 정보 손실이 있을 수 있습니다
  • GPU 메모리가 부족하다면 패치 크기를 32x32로 늘려보세요

2. Patch Embedding

김개발 씨가 패치 분할 코드를 돌려보니 196개의 패치가 생성되었습니다. 그런데 각 패치는 768차원의 벡터였습니다.

"이 숫자들을 어떻게 의미 있는 정보로 만들죠?" 박시니어 씨가 웃으며 답했습니다. "그게 바로 Patch Embedding의 역할이에요."

Patch Embedding은 분할된 이미지 패치를 Transformer가 이해할 수 있는 벡터 표현으로 변환하는 과정입니다. 마치 단어를 Word Embedding으로 바꾸는 것처럼, 이미지 패치를 고차원 벡터 공간에 투영합니다.

일반적으로 선형 변환 레이어를 사용하여 패치의 픽셀 값을 학습 가능한 임베딩 벡터로 매핑합니다. 이 과정에서 패치의 시각적 특징이 수치 벡터로 인코딩됩니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

class PatchEmbedding(nn.Module):
    def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768):
        super().__init__()
        self.num_patches = (img_size // patch_size) ** 2  # 196개

        # Conv2d를 사용한 효율적인 패치 임베딩
        # kernel_size=patch_size, stride=patch_size로 패치 분할과 임베딩을 한번에
        self.projection = nn.Conv2d(
            in_channels, embed_dim,
            kernel_size=patch_size, stride=patch_size
        )

    def forward(self, x):
        # x: (batch, 3, 224, 224)
        x = self.projection(x)  # (batch, 768, 14, 14)
        x = x.flatten(2)  # (batch, 768, 196)
        x = x.transpose(1, 2)  # (batch, 196, 768)
        return x

김개발 씨는 화면에 출력된 텐서 형태를 보며 혼란스러워했습니다. (1, 196, 768)이라는 숫자가 무엇을 의미하는지 알쏭달쏭했습니다.

박시니어 씨가 설명을 이어갔습니다. "자, 생각해봅시다.

우리가 'apple'이라는 단어를 컴퓨터가 이해하게 하려면 어떻게 할까요?" 김개발 씨가 대답했습니다. "Word2Vec 같은 걸로 벡터로 바꾸죠." 정확합니다.

Patch Embedding도 똑같은 원리입니다. 쉽게 비유하자면, Patch Embedding은 마치 외국어 번역기와 같습니다.

한국어로 쓰인 문서를 영어로 번역해야 외국인이 이해하는 것처럼, 이미지 패치를 Transformer가 이해하는 수학적 언어로 번역하는 것입니다. 원본 패치는 단순한 픽셀 값들의 나열이지만, 임베딩을 거치면 의미를 담은 벡터 표현이 됩니다.

이처럼 Patch Embedding도 시각적 정보를 수치적 의미로 변환하는 역할을 담당합니다. Patch Embedding이 제대로 설계되지 않으면 어떤 일이 벌어질까요?

초기 연구들에서는 단순히 패치의 픽셀 값을 평균내거나 합산하는 방식을 시도했습니다. 문제는 이렇게 하면 패치 내부의 세밀한 구조 정보가 모두 손실된다는 것입니다.

예를 들어 강아지의 눈이 있는 패치와 털이 있는 패치가 비슷한 평균 색상을 가지면 구분이 안 됩니다. 더 큰 문제는 학습 가능한 파라미터가 없어서 데이터로부터 더 나은 표현을 배울 수 없다는 점이었습니다.

바로 이런 문제를 해결하기 위해 학습 가능한 선형 변환 레이어가 도입되었습니다. 선형 투영 레이어를 사용하면 패치의 모든 픽셀 정보를 보존하면서도 고차원 공간으로 변환할 수 있습니다.

또한 학습 가능한 가중치를 통해 어떤 시각적 특징이 중요한지 데이터로부터 자동으로 학습합니다. 무엇보다 컨볼루션 레이어를 활용하면 패치 분할과 임베딩을 동시에 수행할 수 있어 효율적입니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 11번째 줄의 Conv2d 레이어가 핵심입니다.

이 레이어는 입력 채널 3개(RGB)를 받아서 768차원의 임베딩 벡터로 변환합니다. kernel_size와 stride를 모두 patch_size로 설정하면, 컨볼루션 연산이 겹치지 않는 패치를 추출하면서 동시에 임베딩하는 효과를 냅니다.

18번째 줄에서는 출력된 특징 맵을 flatten하여 (배치, 768, 196) 형태로 만듭니다. 마지막으로 20번째 줄에서 차원을 바꿔 (배치, 196, 768)로 만드는데, 이는 196개의 패치 토큰이 각각 768차원 벡터를 가진다는 의미입니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 자율주행 차량의 카메라 영상을 분석하는 시스템을 개발한다고 가정해봅시다.

도로의 차선, 신호등, 다른 차량 등을 인식해야 합니다. Patch Embedding을 통해 각 이미지 영역의 특징을 효과적으로 추출할 수 있습니다.

차선이 있는 패치는 직선 패턴을, 신호등이 있는 패치는 원형과 색상 정보를 학습합니다. 테슬라, 웨이모 같은 자율주행 기업들이 유사한 방식으로 비전 시스템을 구축하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 임베딩 차원을 너무 작게 설정하는 것입니다.

embed_dim을 128이나 256으로 줄이면 메모리는 절약되지만, 표현력이 크게 떨어집니다. 이렇게 하면 복잡한 시각적 패턴을 제대로 인코딩할 수 없어 성능이 저하됩니다.

따라서 768 또는 그 이상의 차원을 사용하는 것이 일반적입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 패치를 벡터로 번역하는 거군요!" Patch Embedding을 제대로 이해하면 이미지가 어떻게 Transformer의 언어로 변환되는지 알 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - Conv2d를 사용하면 패치 분할과 임베딩을 한 번에 처리할 수 있어 효율적입니다

  • 임베딩 차원은 768, 1024 등 2의 거듭제곱에 가까운 값을 사용하면 하드웨어 최적화에 유리합니다
  • 사전학습된 ViT 모델을 사용할 때는 임베딩 차원을 함부로 바꾸지 마세요

3. Position Embedding

김개발 씨가 패치 임베딩까지 구현하고 나니 새로운 의문이 생겼습니다. "근데 이 패치들의 순서 정보는 어떻게 알죠?

강아지 얼굴의 왼쪽 눈과 오른쪽 눈이 어디 있는지 구분이 안 되는데요?" 박시니어 씨가 미소지었습니다. "바로 그걸 위한 게 Position Embedding이에요."

Position Embedding은 각 패치의 위치 정보를 임베딩 벡터에 추가하는 기법입니다. Transformer는 본질적으로 순서를 인식하지 못하기 때문에, 명시적으로 위치 정보를 제공해야 합니다.

각 패치의 위치에 해당하는 학습 가능한 벡터를 생성하고, 이를 Patch Embedding에 더해줍니다. 이렇게 하면 모델이 이미지의 공간적 구조를 이해할 수 있게 됩니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

class ViTEmbedding(nn.Module):
    def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768):
        super().__init__()
        num_patches = (img_size // patch_size) ** 2  # 196

        # Patch Embedding
        self.patch_embed = PatchEmbedding(img_size, patch_size, in_channels, embed_dim)

        # CLS 토큰: 전체 이미지를 대표하는 특수 토큰
        self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))

        # Position Embedding: 197개 (196 패치 + 1 CLS 토큰)
        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim))

    def forward(self, x):
        batch_size = x.shape[0]
        x = self.patch_embed(x)  # (batch, 196, 768)

        # CLS 토큰을 배치 크기만큼 확장하여 앞에 추가
        cls_tokens = self.cls_token.expand(batch_size, -1, -1)
        x = torch.cat([cls_tokens, x], dim=1)  # (batch, 197, 768)

        # Position Embedding 더하기
        x = x + self.pos_embed  # (batch, 197, 768)
        return x

김개발 씨는 노트북을 열고 간단한 실험을 해봤습니다. 패치의 순서를 무작위로 섞어도 Transformer는 같은 결과를 출력했습니다.

"이상하네요. 순서가 바뀌었는데 똑같아요." 박시니어 씨가 설명을 시작했습니다.

"그게 바로 Transformer의 특성입니다. Self-Attention은 순서와 상관없이 모든 토큰을 동시에 봅니다." 그렇다면 어떻게 위치를 알려줄 수 있을까요?

쉽게 비유하자면, Position Embedding은 마치 좌석 번호판과 같습니다. 영화관에서 관객들이 무작위로 앉아 있어도, 각 좌석에 번호판이 붙어있으면 "3번 좌석에 앉은 사람"을 식별할 수 있습니다.

좌석 자체는 똑같이 생겼지만, 번호판이 위치 정보를 제공하는 것입니다. 이처럼 Position Embedding도 각 패치에 고유한 위치 표시를 달아주어 모델이 공간적 관계를 파악할 수 있게 합니다.

Position Embedding이 없다면 어떻게 될까요? 초기 실험에서 위치 정보 없이 ViT를 학습시켰더니 성능이 형편없었습니다.

모델은 강아지의 눈, 코, 입이 어디에 있는지 구분하지 못했습니다. 모든 패치가 뒤섞여 있어도 같은 출력을 내놓았습니다.

더 심각한 문제는 좌우 대칭이나 회전에 민감하지 않아, 뒤집힌 이미지와 정상 이미지를 같다고 판단했습니다. 이는 실용적인 모델이 될 수 없었습니다.

바로 이런 문제를 해결하기 위해 Position Embedding이 도입되었습니다. 학습 가능한 위치 벡터를 사용하면 모델이 데이터로부터 최적의 위치 표현을 학습합니다.

또한 CLS 토큰이라는 특수 토큰을 패치 시퀀스 맨 앞에 추가하여 전체 이미지의 정보를 집약합니다. 무엇보다 덧셈 연산만으로 위치 정보를 주입하므로 추가 계산 비용이 거의 없다는 장점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 13번째 줄에서 CLS 토큰을 정의합니다.

이는 (1, 1, 768) 크기의 학습 가능한 파라미터로, 전체 이미지를 대표하는 특수한 토큰입니다. 자연어 처리의 BERT에서 [CLS] 토큰과 같은 역할을 합니다.

16번째 줄에서는 197개 위치(196 패치 + 1 CLS)에 대한 Position Embedding을 초기화합니다. 23번째 줄에서 CLS 토큰을 배치 크기만큼 복사하여 패치 시퀀스 앞에 붙입니다.

마지막으로 27번째 줄에서 Position Embedding을 덧셈으로 더해주는데, 이것이 핵심입니다. 단순 덧셈이지만 각 패치는 자신의 위치 정보를 갖게 됩니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 문서 이미지 인식 시스템을 개발한다고 가정해봅시다.

계약서나 영수증에서 날짜, 금액, 서명 등을 추출해야 합니다. Position Embedding 덕분에 모델은 "왼쪽 위 모서리에 날짜가 있고, 오른쪽 아래에 서명이 있다"는 공간적 패턴을 학습할 수 있습니다.

날짜가 있는 위치의 패치와 금액이 있는 위치의 패치가 다른 위치 임베딩을 갖기 때문입니다. 네이버, 카카오 같은 기업들이 OCR 서비스에서 이런 기법을 활용하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 고정된 삼각함수 기반 Position Encoding을 사용하는 것입니다.

Transformer 원 논문에서는 sin, cos 함수를 사용했지만, ViT에서는 학습 가능한 임베딩이 더 좋은 성능을 보입니다. 이미지는 자연어와 달리 2D 구조를 가지므로, 모델이 스스로 최적의 위치 표현을 배우는 것이 유리합니다.

따라서 nn.Parameter로 학습 가능하게 만드는 것이 권장됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 각 패치에 좌석 번호를 달아주는 거군요!" Position Embedding을 제대로 이해하면 ViT가 어떻게 이미지의 공간 정보를 학습하는지 알 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 학습 가능한 Position Embedding이 고정된 삼각함수 방식보다 ViT에서 성능이 좋습니다

  • CLS 토큰은 분류 작업에서 최종 출력으로 사용되므로 매우 중요합니다
  • Position Embedding을 초기화할 때 평균 0, 작은 분산으로 시작하면 학습이 안정적입니다

4. Self-Attention in Vision

김개발 씨가 드디어 Transformer의 핵심인 Self-Attention을 구현하려고 합니다. "자연어에서는 단어들 사이의 관계를 본다던데, 이미지에서는 뭘 보는 거죠?" 박시니어 씨가 흥미로운 예시를 들었습니다.

"강아지 사진에서 '눈' 패치와 '코' 패치가 서로 연관되어 있다는 걸 찾아냅니다."

Self-Attention in Vision은 이미지의 서로 다른 패치들 사이의 관계를 학습하는 메커니즘입니다. 각 패치가 다른 모든 패치를 참조하여 자신의 표현을 업데이트합니다.

Query, Key, Value라는 세 가지 벡터를 사용하여 패치 간 유사도를 계산하고, 중요한 패치에 더 많은 가중치를 부여합니다. 이를 통해 모델은 이미지의 전역적 문맥을 파악할 수 있습니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

class MultiHeadSelfAttention(nn.Module):
    def __init__(self, embed_dim=768, num_heads=12):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = embed_dim // num_heads  # 768 / 12 = 64

        # Query, Key, Value 변환을 위한 선형 레이어
        self.qkv = nn.Linear(embed_dim, embed_dim * 3)
        self.proj = nn.Linear(embed_dim, embed_dim)

    def forward(self, x):
        batch, num_patches, embed_dim = x.shape  # (B, 197, 768)

        # QKV 계산: (B, 197, 768) -> (B, 197, 2304)
        qkv = self.qkv(x)
        # 3개로 분리: (B, 197, 768) x 3
        qkv = qkv.reshape(batch, num_patches, 3, self.num_heads, self.head_dim)
        qkv = qkv.permute(2, 0, 3, 1, 4)  # (3, B, 12, 197, 64)
        q, k, v = qkv[0], qkv[1], qkv[2]  # 각각 (B, 12, 197, 64)

        # Attention 점수 계산: Q @ K^T / sqrt(d_k)
        attn = (q @ k.transpose(-2, -1)) * (self.head_dim ** -0.5)  # (B, 12, 197, 197)
        attn = attn.softmax(dim=-1)

        # Value와 가중합: (B, 12, 197, 197) @ (B, 12, 197, 64) = (B, 12, 197, 64)
        x = attn @ v
        x = x.transpose(1, 2).reshape(batch, num_patches, embed_dim)  # (B, 197, 768)
        x = self.proj(x)
        return x

김개발 씨는 CNN으로만 이미지를 다루다가 Self-Attention을 접하니 머리가 복잡했습니다. "컨볼루션은 주변 픽셀만 보잖아요.

근데 이건 모든 패치를 다 본다고요?" 박시니어 씨가 화이트보드에 그림을 그렸습니다. "맞아요.

그게 바로 Self-Attention의 강력함입니다." CNN과 Self-Attention은 어떻게 다를까요? 쉽게 비유하자면, CNN은 마치 근시인 사람과 같습니다.

3x3 또는 5x5 필터로 주변 영역만 보면서 특징을 추출합니다. 가까운 픽셀들 사이의 관계는 잘 포착하지만, 멀리 떨어진 영역 간의 연결은 여러 레이어를 거쳐야 겨우 파악할 수 있습니다.

반면 Self-Attention은 완벽한 시력을 가진 사람과 같습니다. 이미지의 왼쪽 위 패치와 오른쪽 아래 패치가 멀리 떨어져 있어도 직접적으로 관계를 계산합니다.

이처럼 Self-Attention은 전역적 시야를 가진 메커니즘입니다. Self-Attention이 비전에 적용되기 전에는 어땠을까요?

CNN은 지역적 특징 추출에는 탁월했지만, 전역적 문맥을 이해하는 데 한계가 있었습니다. 예를 들어 강아지 얼굴을 인식할 때, 귀와 꼬리가 동시에 나타나는 패턴을 학습하려면 매우 깊은 네트워크가 필요했습니다.

더 큰 문제는 이미지 크기가 달라지면 학습한 패턴이 일반화되지 않는다는 점이었습니다. 고정된 필터 크기로는 다양한 스케일의 객체를 효과적으로 다루기 어려웠습니다.

바로 이런 문제를 해결하기 위해 Self-Attention이 비전에 도입되었습니다. 전역적 수용 영역을 통해 첫 레이어부터 이미지 전체를 볼 수 있습니다.

또한 동적인 가중치 계산으로 각 패치가 다른 패치에 얼마나 집중할지 내용 기반으로 결정합니다. 무엇보다 위치에 구애받지 않는 패턴 매칭이 가능하여, 객체가 이미지 어디에 있든 인식할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 11번째 줄에서 하나의 선형 레이어로 Query, Key, Value를 한 번에 계산합니다.

입력 차원 768을 2304(768 x 3)로 변환하여 효율성을 높입니다. 20번째 줄에서는 계산된 QKV를 12개의 헤드로 분리합니다.

Multi-Head Attention은 여러 개의 작은 Attention을 병렬로 수행하여 다양한 관점에서 관계를 학습합니다. 25번째 줄의 Attention 점수 계산이 핵심입니다.

Query와 Key의 내적을 계산하여 패치 간 유사도를 구하고, head_dim의 제곱근으로 나누어 안정성을 확보합니다. 그 다음 softmax로 정규화하여 확률 분포를 만듭니다.

29번째 줄에서는 Attention 가중치를 Value에 곱하여 최종 출력을 계산합니다. 중요한 패치의 정보가 더 많이 반영됩니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 패션 이커머스의 유사 상품 추천 시스템을 개발한다고 가정해봅시다.

사용자가 빨간 원피스를 클릭하면, 비슷한 스타일의 옷을 찾아야 합니다. Self-Attention을 사용하면 옷의 색상, 패턴, 실루엣 등 여러 속성을 동시에 고려하여 유사도를 계산할 수 있습니다.

원피스의 윗부분 패치가 아래부분 패치와 어울리는지, 소매 패턴이 전체 디자인과 조화를 이루는지 등을 자동으로 학습합니다. 무신사, 에이블리 같은 패션 플랫폼들이 이런 기술을 활용하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 헤드 개수를 너무 많이 설정하는 것입니다.

num_heads를 32나 64로 늘리면 각 헤드의 차원(head_dim)이 너무 작아져서 표현력이 떨어집니다. 이렇게 하면 계산량만 늘고 성능 향상은 미미합니다.

따라서 8 또는 12개가 일반적으로 권장되는 헤드 개수입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 모든 패치가 서로에게 질문하고 답하는 거군요!" Self-Attention을 제대로 이해하면 ViT가 왜 강력한지 알 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - Multi-Head Attention은 12개 헤드가 일반적이며, 각 헤드는 다른 관점의 관계를 학습합니다

  • Attention Map을 시각화하면 모델이 어느 부분을 중요하게 보는지 확인할 수 있습니다
  • QKV를 한 번에 계산하는 것이 따로 계산하는 것보다 효율적입니다

5. ViT 아키텍처 구조

김개발 씨가 지금까지 배운 모든 구성요소를 조합하려고 하니 막막했습니다. "패치 분할, 임베딩, Self-Attention...

이걸 어떻게 연결하죠?" 박시니어 씨가 전체 구조 다이어그램을 보여주며 말했습니다. "하나하나는 이미 만들었어요.

이제 레고 블록처럼 조립만 하면 됩니다."

ViT 아키텍처는 Transformer Encoder를 이미지 분류에 적용한 완전한 구조입니다. 입력 이미지를 패치로 분할하고, 임베딩을 거쳐, 여러 개의 Transformer Encoder 블록을 통과시킵니다.

각 Encoder 블록은 Multi-Head Self-Attention과 MLP로 구성되며, Residual Connection과 Layer Normalization이 추가됩니다. 최종적으로 CLS 토큰의 출력을 분류 헤드에 통과시켜 예측을 수행합니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

class TransformerEncoderBlock(nn.Module):
    def __init__(self, embed_dim=768, num_heads=12, mlp_ratio=4.0):
        super().__init__()
        self.norm1 = nn.LayerNorm(embed_dim)
        self.attn = MultiHeadSelfAttention(embed_dim, num_heads)
        self.norm2 = nn.LayerNorm(embed_dim)

        # MLP: 768 -> 3072 -> 768
        mlp_hidden_dim = int(embed_dim * mlp_ratio)
        self.mlp = nn.Sequential(
            nn.Linear(embed_dim, mlp_hidden_dim),
            nn.GELU(),  # Transformer에서 선호되는 활성화 함수
            nn.Linear(mlp_hidden_dim, embed_dim)
        )

    def forward(self, x):
        # Residual Connection with Pre-Normalization
        x = x + self.attn(self.norm1(x))  # Self-Attention
        x = x + self.mlp(self.norm2(x))   # MLP
        return x

class VisionTransformer(nn.Module):
    def __init__(self, img_size=224, patch_size=16, num_classes=1000,
                 embed_dim=768, depth=12, num_heads=12):
        super().__init__()
        self.embedding = ViTEmbedding(img_size, patch_size, 3, embed_dim)

        # 12개의 Transformer Encoder 블록
        self.blocks = nn.ModuleList([
            TransformerEncoderBlock(embed_dim, num_heads) for _ in range(depth)
        ])

        self.norm = nn.LayerNorm(embed_dim)
        # 분류 헤드: CLS 토큰 -> 클래스 예측
        self.head = nn.Linear(embed_dim, num_classes)

    def forward(self, x):
        x = self.embedding(x)  # (B, 197, 768)

        for block in self.blocks:
            x = block(x)  # 12번 반복

        x = self.norm(x)
        # CLS 토큰만 추출하여 분류
        cls_token_output = x[:, 0]  # (B, 768)
        logits = self.head(cls_token_output)  # (B, 1000)
        return logits

김개발 씨는 코드를 실행하며 감탄했습니다. "와, 이게 정말 작동하네요!" 입력으로 고양이 사진을 넣자 "tabby cat"이라는 예측이 나왔습니다.

박시니어 씨가 웃으며 말했습니다. "이제 ViT의 전체 흐름을 이해하셨네요." ViT는 어떻게 이미지를 처리할까요?

쉽게 비유하자면, ViT는 마치 회의에서 의견을 수렴하는 과정과 같습니다. 먼저 참석자들(패치)이 회의실에 모입니다(패치 임베딩).

각자 자기소개를 하며 자신의 위치를 밝힙니다(Position Embedding). 그 다음 여러 차례 토론을 진행하는데(Transformer Encoder 블록), 각 라운드마다 참석자들이 서로의 의견을 듣고 자신의 입장을 업데이트합니다(Self-Attention).

중간중간 각자 심화 분석도 합니다(MLP). 마지막으로 의장(CLS 토큰)이 모든 의견을 종합하여 최종 결론을 내립니다(분류 헤드).

이처럼 ViT도 여러 단계를 거쳐 이미지 정보를 점진적으로 정제합니다. ViT가 등장하기 전 이미지 분류는 어땠을까요?

ResNet, EfficientNet 같은 CNN 기반 모델들이 주류였습니다. 이들은 컨볼루션 레이어를 수십 개 쌓아서 계층적 특징을 학습했습니다.

문제는 수용 영역이 레이어 깊이에 따라 선형적으로 증가한다는 점이었습니다. 이미지 전체를 보려면 100개 이상의 레이어가 필요했습니다.

더 큰 문제는 귀납적 편향이 너무 강하다는 것입니다. CNN은 지역성과 평행이동 불변성을 가정하는데, 이것이 항상 최적은 아닙니다.

바로 이런 한계를 극복하기 위해 ViT가 제안되었습니다. 첫 레이어부터 전역 수용 영역을 가져 이미지 전체를 볼 수 있습니다.

또한 데이터 기반 학습으로 귀납적 편향을 최소화하고, 충분한 데이터가 있으면 모델이 스스로 최적의 패턴을 찾습니다. 무엇보다 확장성이 뛰어나 모델 크기를 키울수록 성능이 일관되게 향상됩니다.

대규모 데이터셋에서 사전학습하면 CNN을 능가하는 성능을 보입니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 Transformer Encoder 블록의 구조를 보겠습니다. 21번째 줄에서 Pre-Normalization 패턴을 사용합니다.

Attention 전에 Layer Norm을 적용하고, 원본 입력을 더하는 Residual Connection을 사용합니다. 이는 원래 Transformer의 Post-Norm보다 학습이 안정적입니다.

22번째 줄도 마찬가지로 MLP 전에 정규화를 적용합니다. 15번째 줄의 GELU 활성화 함수는 ReLU보다 부드러운 곡선을 가져 Transformer에서 선호됩니다.

VisionTransformer 클래스의 32번째 줄에서는 12개의 Encoder 블록을 순차적으로 쌓습니다. ViT-Base 모델의 표준 깊이입니다.

49번째 줄에서는 최종 출력의 첫 번째 토큰, 즉 CLS 토큰만 추출하여 분류에 사용합니다. 나머지 196개 패치 토큰은 버립니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 의료 영상 진단 시스템을 개발한다고 가정해봅시다.

CT 스캔 이미지에서 암을 조기 발견해야 합니다. ViT를 사전학습한 후 의료 데이터로 Fine-tuning하면, 이미지 전체의 미묘한 패턴을 포착할 수 있습니다.

암 조직은 작고 흩어져 있을 수 있는데, Self-Attention이 멀리 떨어진 병변들 간의 연관성을 찾아냅니다. 또한 Attention Map을 시각화하면 의사에게 진단 근거를 제시할 수 있어 신뢰성이 높아집니다.

구글 헬스, 루닛 같은 기업들이 이런 방식으로 의료 AI를 개발하고 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 작은 데이터셋에서 처음부터 ViT를 학습시키는 것입니다. ViT는 귀납적 편향이 적어 충분한 데이터 없이는 수렴하지 않습니다.

ImageNet-21K 같은 대규모 데이터셋에서 사전학습한 모델을 사용하거나, 데이터가 적다면 CNN을 선택하는 것이 낫습니다. 따라서 Transfer Learning을 활용하는 것이 필수입니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 전체 구조가 이렇게 연결되는 거군요!" ViT 아키텍처를 제대로 이해하면 다른 Vision Transformer 모델들도 쉽게 이해할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - ViT-Base는 12 레이어, ViT-Large는 24 레이어입니다. 데이터가 많을수록 큰 모델이 유리합니다

  • 사전학습된 모델을 HuggingFace에서 다운로드하여 Fine-tuning하는 것을 권장합니다
  • Batch Size를 크게 설정할수록 성능이 향상되지만, GPU 메모리를 많이 사용합니다

6. CNN vs ViT 비교

김개발 씨가 ViT를 어느 정도 이해하고 나니 새로운 고민이 생겼습니다. "그럼 앞으로 CNN은 안 쓰나요?

ViT가 무조건 좋은 건가요?" 박시니어 씨가 고개를 저었습니다. "아니요.

각각 장단점이 있어서, 상황에 맞게 선택해야 합니다."

CNN과 ViT의 비교는 귀납적 편향, 데이터 효율성, 계산 복잡도, 성능 등 여러 측면에서 이루어집니다. CNN은 지역성과 평행이동 불변성이라는 강한 가정을 내장하고 있어 적은 데이터로도 잘 학습됩니다.

반면 ViT는 이런 가정이 적어 대규모 데이터에서 더 나은 일반화 성능을 보입니다. 각 모델의 특성을 이해하고 프로젝트 요구사항에 맞게 선택하는 것이 중요합니다.

다음 코드를 살펴봅시다.

import torch
import torchvision.models as models
from transformers import ViTForImageClassification

# CNN 예시: ResNet50
cnn_model = models.resnet50(pretrained=True)
cnn_model.eval()

# ViT 예시: ViT-Base
vit_model = ViTForImageClassification.from_pretrained('google/vit-base-patch16-224')
vit_model.eval()

# 동일한 입력으로 추론 시간 비교
dummy_input = torch.randn(1, 3, 224, 224)

import time

# CNN 추론 시간
start = time.time()
with torch.no_grad():
    cnn_output = cnn_model(dummy_input)
cnn_time = time.time() - start

# ViT 추론 시간
start = time.time()
with torch.no_grad():
    vit_output = vit_model(dummy_input).logits
vit_time = time.time() - start

print(f"CNN 추론 시간: {cnn_time:.4f}초")
print(f"ViT 추론 시간: {vit_time:.4f}초")
print(f"CNN 파라미터 수: {sum(p.numel() for p in cnn_model.parameters()) / 1e6:.1f}M")
print(f"ViT 파라미터 수: {sum(p.numel() for p in vit_model.parameters()) / 1e6:.1f}M")

김개발 씨는 두 모델의 성능을 비교하는 실험을 돌려봤습니다. ImageNet-1K에서 처음부터 학습시키니 ResNet이 더 빨리 수렴했습니다.

하지만 ImageNet-21K에서 사전학습한 ViT를 Fine-tuning하니 최종 정확도가 더 높았습니다. 박시니어 씨가 결과를 보며 설명했습니다.

"이게 바로 두 모델의 핵심 차이점입니다." CNN과 ViT는 본질적으로 어떻게 다를까요? 쉽게 비유하자면, CNN은 마치 숙련된 장인과 같습니다.

오랜 경험으로 체득한 노하우(귀납적 편향)가 있어, 비슷한 상황을 적은 경험으로도 잘 처리합니다. 목공일을 10년 한 장인은 새로운 가구를 만들 때도 익숙한 기법을 바로 적용합니다.

반면 ViT는 백지 상태의 천재와 같습니다. 선입견이 없어 매우 많은 경험(데이터)이 필요하지만, 충분히 배우고 나면 장인보다 더 창의적이고 유연한 해결책을 찾아냅니다.

이처럼 CNN은 효율성을, ViT는 잠재력을 대표합니다. 두 모델의 구체적인 차이점을 살펴보겠습니다.

첫째, 귀납적 편향 측면에서 CNN은 지역성을 가정합니다. 가까운 픽셀들이 관련이 있다는 것입니다.

또한 평행이동 불변성이 내장되어 있어, 객체가 이미지 어디에 있든 같은 필터로 감지합니다. 반면 ViT는 이런 가정이 없어 모든 것을 데이터로부터 학습합니다.

둘째, 데이터 효율성에서 CNN은 ImageNet-1K(130만 장)만으로도 좋은 성능을 냅니다. ViT는 ImageNet-21K(1400만 장) 이상이 필요합니다.

데이터가 적으면 과적합되기 쉽습니다. 셋째, 계산 복잡도 측면을 보겠습니다.

CNN의 컨볼루션 연산은 입력 크기에 선형 비례합니다. 이미지가 2배 커지면 계산량도 약 2배 증가합니다.

반면 ViT의 Self-Attention은 패치 개수의 제곱에 비례합니다. 패치가 2배 많아지면 계산량은 4배 증가합니다.

따라서 고해상도 이미지에서는 CNN이 효율적입니다. 넷째, 성능에서는 소규모 데이터셋에서 CNN이 우세합니다.

하지만 대규모 데이터셋에서 사전학습하면 ViT가 더 높은 정확도를 달성합니다. ViT-Huge 모델은 ImageNet에서 CNN보다 1-2% 높은 정확도를 보입니다.

위의 코드를 한 줄씩 살펴보겠습니다. 6번째 줄에서 ResNet50 모델을 불러옵니다.

ResNet은 잔차 연결을 사용한 대표적인 CNN 아키텍처로, 50개의 레이어를 가집니다. 10번째 줄에서는 HuggingFace에서 사전학습된 ViT-Base 모델을 로드합니다.

이 모델은 ImageNet-21K에서 사전학습되어 강력한 특징 추출 능력을 갖췄습니다. 19-28번째 줄에서는 두 모델의 추론 시간을 측정합니다.

일반적으로 ResNet이 약간 빠르지만, GPU 최적화에 따라 차이가 줄어들 수 있습니다. 31-34번째 줄에서는 파라미터 개수를 출력합니다.

ResNet50은 약 25M, ViT-Base는 약 86M으로 ViT가 3배 이상 크지만, 대규모 데이터에서는 이 크기가 장점이 됩니다. 실제 현업에서는 어떻게 선택할까요?

예를 들어 모바일 앱에서 실시간 객체 인식을 구현한다고 가정해봅시다. 이 경우 CNN이 유리합니다.

MobileNet이나 EfficientNet 같은 경량 CNN은 스마트폰에서도 빠르게 동작하며, 적은 메모리를 사용합니다. 데이터도 상대적으로 적게 필요하므로 특정 도메인(예: 식물 인식)에 빠르게 적응할 수 있습니다.

반면 대규모 이미지 검색 엔진을 개발한다면 ViT가 적합합니다. 서버에서 배치 처리하므로 계산 비용이 덜 중요하고, 수억 장의 이미지로 학습할 수 있어 ViT의 강점이 극대화됩니다.

구글 이미지 검색, 핀터레스트 같은 서비스에서 ViT 계열 모델을 활용합니다. 최근에는 두 모델의 장점을 결합하려는 시도도 있습니다.

Hybrid 모델은 초기 레이어에 CNN을 사용해 귀납적 편향을 주입하고, 후기 레이어에 Transformer를 사용합니다. 이렇게 하면 적은 데이터로도 ViT의 성능에 근접할 수 있습니다.

ConvNeXt 같은 모델은 CNN 아키텍처를 현대화하여 ViT와 유사한 성능을 내면서도 효율성을 유지합니다. 연구자들은 여전히 두 패러다임의 최적 조합을 찾고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 무조건 최신 모델을 선택하는 것입니다.

ViT가 논문에서 좋은 결과를 냈다고 해서 모든 상황에 적합한 것은 아닙니다. 데이터 규모, 하드웨어 자원, 추론 속도 요구사항 등을 종합적으로 고려해야 합니다.

따라서 프로젝트 요구사항을 먼저 분석하고 적합한 모델을 선택하세요. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 상황에 맞게 선택하는 게 중요하군요!" CNN과 ViT의 차이를 제대로 이해하면 프로젝트에 최적의 모델을 선택할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 데이터가 10만 장 미만이면 CNN 또는 사전학습된 ViT Fine-tuning을 권장합니다

  • 실시간 추론이 필요한 엣지 디바이스에서는 CNN이 여전히 우위입니다
  • ViT는 배치 크기를 크게 설정해야 성능이 극대화되므로, GPU 메모리가 충분한 환경에 적합합니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#ViT#Transformer#Patch Embedding#Position Embedding#Self-Attention#ViT,Transformer,Computer Vision

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.