CHAPTER 3

CNN과 비전 트랜스포머

현대 비전 AI의 핵심 아키텍처

합성곱 신경망 (CNN) 기초

CNN은 이미지 처리에 특화된 딥러닝 아키텍처로, 지역적 연결성과 파라미터 공유를 통해 효율적으로 시각적 특징을 학습합니다.

CNN의 핵심 레이어

import torch
import torch.nn as nn
import torch.nn.functional as F

class CNN(nn.Module):
    def __init__(self, num_classes=10):
        super(CNN, self).__init__()

        # 첫 번째 합성곱 블록
        self.conv1 = nn.Conv2d(
            in_channels=3,    # RGB 입력
            out_channels=32,  # 32개의 필터
            kernel_size=3,    # 3x3 필터
            padding=1         # 같은 크기 유지
        )
        self.bn1 = nn.BatchNorm2d(32)

        # 두 번째 합성곱 블록
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(64)

        # 세 번째 합성곱 블록
        self.conv3 = nn.Conv2d(64, 128, 3, padding=1)
        self.bn3 = nn.BatchNorm2d(128)

        # 풀링
        self.pool = nn.MaxPool2d(2, 2)

        # 완전 연결 레이어
        self.fc1 = nn.Linear(128 * 28 * 28, 512)
        self.fc2 = nn.Linear(512, num_classes)

        # 드롭아웃
        self.dropout = nn.Dropout(0.5)

    def forward(self, x):
        # 블록 1: Conv -> BN -> ReLU -> Pool
        x = self.conv1(x)
        x = self.bn1(x)
        x = F.relu(x)
        x = self.pool(x)  # 224 -> 112

        # 블록 2
        x = self.conv2(x)
        x = self.bn2(x)
        x = F.relu(x)
        x = self.pool(x)  # 112 -> 56

        # 블록 3
        x = self.conv3(x)
        x = self.bn3(x)
        x = F.relu(x)
        x = self.pool(x)  # 56 -> 28

        # 평탄화
        x = x.view(x.size(0), -1)

        # 완전 연결 레이어
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout(x)
        x = self.fc2(x)

        return x

# 모델 생성 및 확인
model = CNN(num_classes=10)
print(model)

# 입력 텐서로 테스트
dummy_input = torch.randn(1, 3, 224, 224)
output = model(dummy_input)
print(f"출력 shape: {output.shape}")  # [1, 10]

유명한 CNN 아키텍처

LeNet-5 (1998)

최초의 성공적인 CNN, 손글씨 숫자 인식에 사용

AlexNet (2012)

ImageNet 대회에서 우승하며 딥러닝 혁명을 시작

VGGNet (2014)

3x3 작은 필터를 깊게 쌓은 구조

# VGG-16 스타일 블록
class VGGBlock(nn.Module):
    def __init__(self, in_channels, out_channels, num_convs):
        super().__init__()
        layers = []
        for _ in range(num_convs):
            layers.append(nn.Conv2d(in_channels, out_channels, 3, padding=1))
            layers.append(nn.BatchNorm2d(out_channels))
            layers.append(nn.ReLU(inplace=True))
            in_channels = out_channels
        layers.append(nn.MaxPool2d(2, 2))
        self.block = nn.Sequential(*layers)

    def forward(self, x):
        return self.block(x)

GoogLeNet / Inception (2014)

Inception 모듈로 다중 스케일 특징 추출

class InceptionModule(nn.Module):
    def __init__(self, in_channels):
        super().__init__()

        # 1x1 경로
        self.branch1 = nn.Conv2d(in_channels, 64, 1)

        # 1x1 -> 3x3 경로
        self.branch2 = nn.Sequential(
            nn.Conv2d(in_channels, 96, 1),
            nn.Conv2d(96, 128, 3, padding=1)
        )

        # 1x1 -> 5x5 경로
        self.branch3 = nn.Sequential(
            nn.Conv2d(in_channels, 16, 1),
            nn.Conv2d(16, 32, 5, padding=2)
        )

        # 3x3 MaxPool -> 1x1 경로
        self.branch4 = nn.Sequential(
            nn.MaxPool2d(3, stride=1, padding=1),
            nn.Conv2d(in_channels, 32, 1)
        )

    def forward(self, x):
        branch1 = self.branch1(x)
        branch2 = self.branch2(x)
        branch3 = self.branch3(x)
        branch4 = self.branch4(x)

        # 채널 차원으로 연결
        outputs = [branch1, branch2, branch3, branch4]
        return torch.cat(outputs, 1)

ResNet (2015)

잔차 연결(Residual Connection)로 매우 깊은 네트워크 학습 가능

class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()

        # 주 경로
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, padding=1)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)

        # 스킵 연결 (차원 조정 필요시)
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, 1, stride),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        # 잔차 학습
        residual = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = F.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        # 스킵 연결 추가
        out += self.shortcut(residual)
        out = F.relu(out)

        return out

DenseNet (2017)

각 레이어가 모든 이전 레이어와 연결

EfficientNet (2019)

복합 스케일링으로 효율성과 정확도 균형

Vision Transformer (ViT)

NLP의 트랜스포머를 비전에 적용한 혁신적인 아키텍처. 이미지를 패치로 나누어 시퀀스로 처리합니다.

ViT 아키텍처

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.img_size = img_size
        self.patch_size = patch_size
        self.n_patches = (img_size // patch_size) ** 2

        # 합성곱으로 패치 추출 및 임베딩
        self.proj = nn.Conv2d(
            in_channels,
            embed_dim,
            kernel_size=patch_size,
            stride=patch_size
        )

    def forward(self, x):
        # x: [batch, 3, 224, 224]
        x = self.proj(x)  # [batch, embed_dim, 14, 14]
        x = x.flatten(2)   # [batch, embed_dim, 196]
        x = x.transpose(1, 2)  # [batch, 196, embed_dim]
        return x

class MultiHeadSelfAttention(nn.Module):
    """멀티헤드 셀프 어텐션"""
    def __init__(self, embed_dim=768, num_heads=12):
        super().__init__()
        self.embed_dim = embed_dim
        self.num_heads = num_heads
        self.head_dim = embed_dim // num_heads

        self.qkv = nn.Linear(embed_dim, embed_dim * 3)
        self.proj = nn.Linear(embed_dim, embed_dim)

    def forward(self, x):
        batch_size, n_tokens, embed_dim = x.shape

        # Q, K, V 계산
        qkv = self.qkv(x)  # [batch, n_tokens, embed_dim * 3]
        qkv = qkv.reshape(batch_size, n_tokens, 3, self.num_heads, self.head_dim)
        qkv = qkv.permute(2, 0, 3, 1, 4)  # [3, batch, num_heads, n_tokens, head_dim]
        q, k, v = qkv[0], qkv[1], qkv[2]

        # 어텐션 스코어 계산
        attn = (q @ k.transpose(-2, -1)) / (self.head_dim ** 0.5)
        attn = attn.softmax(dim=-1)

        # 어텐션 적용
        out = attn @ v  # [batch, num_heads, n_tokens, head_dim]
        out = out.transpose(1, 2)  # [batch, n_tokens, num_heads, head_dim]
        out = out.reshape(batch_size, n_tokens, embed_dim)

        # 출력 프로젝션
        out = self.proj(out)
        return out

class TransformerBlock(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_hidden_dim = int(embed_dim * mlp_ratio)
        self.mlp = nn.Sequential(
            nn.Linear(embed_dim, mlp_hidden_dim),
            nn.GELU(),
            nn.Linear(mlp_hidden_dim, embed_dim)
        )

    def forward(self, x):
        # 어텐션 + 잔차 연결
        x = x + self.attn(self.norm1(x))

        # MLP + 잔차 연결
        x = x + self.mlp(self.norm2(x))

        return x

class VisionTransformer(nn.Module):
    """Vision Transformer"""
    def __init__(
        self,
        img_size=224,
        patch_size=16,
        in_channels=3,
        num_classes=1000,
        embed_dim=768,
        depth=12,
        num_heads=12,
        mlp_ratio=4.0
    ):
        super().__init__()

        # 패치 임베딩
        self.patch_embed = PatchEmbedding(img_size, patch_size, in_channels, embed_dim)
        n_patches = self.patch_embed.n_patches

        # CLS 토큰 (분류용)
        self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))

        # 위치 임베딩
        self.pos_embed = nn.Parameter(torch.zeros(1, n_patches + 1, embed_dim))

        # 트랜스포머 블록들
        self.blocks = nn.ModuleList([
            TransformerBlock(embed_dim, num_heads, mlp_ratio)
            for _ in range(depth)
        ])

        self.norm = nn.LayerNorm(embed_dim)

        # 분류 헤드
        self.head = nn.Linear(embed_dim, num_classes)

    def forward(self, x):
        batch_size = x.shape[0]

        # 패치 임베딩
        x = self.patch_embed(x)  # [batch, n_patches, embed_dim]

        # CLS 토큰 추가
        cls_token = self.cls_token.expand(batch_size, -1, -1)
        x = torch.cat([cls_token, x], dim=1)  # [batch, n_patches+1, embed_dim]

        # 위치 임베딩 추가
        x = x + self.pos_embed

        # 트랜스포머 블록들 통과
        for block in self.blocks:
            x = block(x)

        x = self.norm(x)

        # CLS 토큰으로 분류
        cls_output = x[:, 0]
        output = self.head(cls_output)

        return output

# ViT 모델 생성
vit = VisionTransformer(
    img_size=224,
    patch_size=16,
    num_classes=1000,
    embed_dim=768,
    depth=12,
    num_heads=12
)

# 테스트
dummy_input = torch.randn(1, 3, 224, 224)
output = vit(dummy_input)
print(f"출력 shape: {output.shape}")  # [1, 1000]

CNN vs Transformer

CNN의 장점

Transformer의 장점

하이브리드 아키텍처

Swin Transformer

계층적 구조와 윈도우 기반 어텐션으로 효율성 개선

ConvNeXt

CNN에 트랜스포머의 설계 원칙을 적용

CoAtNet

합성곱과 어텐션을 효과적으로 결합

실전 활용

전이 학습

import torchvision.models as models

# ImageNet으로 사전학습된 ResNet50 로드
resnet50 = models.resnet50(pretrained=True)

# 마지막 레이어만 파인튜닝 (새 작업 10개 클래스)
num_classes = 10
resnet50.fc = nn.Linear(resnet50.fc.in_features, num_classes)

# 백본 동결 (선택사항)
for param in resnet50.parameters():
    param.requires_grad = False

# 마지막 레이어만 학습 가능
for param in resnet50.fc.parameters():
    param.requires_grad = True

# 옵티마이저 (마지막 레이어만)
optimizer = torch.optim.Adam(resnet50.fc.parameters(), lr=0.001)

모델 앙상블

class EnsembleModel(nn.Module):
    def __init__(self, models):
        super().__init__()
        self.models = nn.ModuleList(models)

    def forward(self, x):
        # 각 모델의 예측 평균
        outputs = [model(x) for model in self.models]
        return torch.stack(outputs).mean(dim=0)

# ResNet, EfficientNet, ViT 앙상블
ensemble = EnsembleModel([
    models.resnet50(pretrained=True),
    models.efficientnet_b0(pretrained=True),
    # vit 모델
])

요약

  • CNN은 합성곱, 풀링, 활성화 함수로 구성된 이미지 특화 아키텍처입니다
  • AlexNet, VGG, ResNet, EfficientNet 등 다양한 CNN 아키텍처가 발전해왔습니다
  • ResNet의 잔차 연결로 매우 깊은 네트워크 학습이 가능해졌습니다
  • Vision Transformer는 이미지를 패치로 나누어 어텐션 메커니즘 적용합니다
  • CNN은 귀납 편향이 강하고, Transformer는 대규모 데이터에서 뛰어납니다
  • Swin Transformer, ConvNeXt 등 하이브리드 아키텍처가 등장했습니다
  • 전이 학습으로 사전학습 모델을 새 작업에 효율적으로 활용합니다
  • 弘益人間 - 강력한 비전 모델로 의료 진단, 재난 감지 등 사회에 기여합니다

복습 문제

  1. CNN의 합성곱 레이어가 완전 연결 레이어보다 이미지 처리에 적합한 이유는 무엇인가요?
  2. ResNet의 잔차 연결(Residual Connection)이 해결하는 문제는 무엇인가요?
  3. Inception 모듈이 다중 스케일 특징을 추출하는 방법을 설명하세요.
  4. Vision Transformer가 이미지를 처리하는 방식을 단계별로 설명하세요.
  5. CNN과 Transformer의 주요 차이점과 각각의 장단점은 무엇인가요?
  6. 전이 학습이 무엇이며, 어떤 상황에서 유용한가요?
  7. 배치 정규화(Batch Normalization)의 역할과 효과를 설명하세요.