Skip to main content
Summary

이미지를 한 줄로 펼치면 사라지는 것

이전 장에서 우리는 MLP로 손글씨 숫자를 분류했습니다. 이미지를 텐서로 받고, 한 줄로 펼친 뒤, 선형층과 활성화 함수를 지나게 했습니다. 그 과정은 딥러닝 모델을 처음 완성해 보는 데 아주 좋은 출발점이었습니다.

하지만 이미지에는 MLP가 놓치기 쉬운 중요한 정보가 있습니다. 바로 공간 구조입니다.

예를 들어 고양이 사진을 생각해 봅시다. 왼쪽 위에는 귀가 있고, 가운데에는 눈이 있고, 아래쪽에는 입이 있을 수 있습니다. 가까이 붙어 있는 픽셀들은 함께 모여 선, 모서리, 질감, 곡선 같은 패턴을 만듭니다. 그런데 MLP는 이미지를 다음처럼 한 줄로 펼쳐 봅니다.

# 3 x 32 x 32 이미지를 한 줄로 펼치기
x = x.view(batch_size, -1)  # 3072개의 숫자

이렇게 펼치면 모델은 여전히 모든 픽셀 값을 볼 수 있습니다. 하지만 원래 가까이 있던 픽셀들이 어떤 위치 관계를 가졌는지 직접적으로 활용하기 어렵습니다. 2차원 지도였던 이미지를 긴 영수증처럼 펼쳐 버리는 셈입니다.

CNN, 즉 합성곱 신경망은 이 문제에서 출발합니다. CNN은 이미지를 무작정 펼치지 않습니다. 이미지 위를 작은 창으로 훑으며 가까운 픽셀들이 만드는 지역 패턴을 찾습니다. 덕분에 픽셀 하나하나를 외우기보다, 이미지 곳곳에 나타나는 선, 모서리, 색의 변화, 질감 같은 특징을 학습할 수 있습니다.

이 장의 목표는 CNN을 거대한 공식 묶음으로 외우는 것이 아닙니다. CNN이 이미지를 바라보는 방식을 이해하는 것입니다. 핵심은 간단합니다.

이미지 전체를 한 번에 보지 말고, 작은 영역을 반복해서 보며 패턴을 찾는다.

CNN으로 이미지 분류하기

합성곱 필터는 작은 탐정이다

CNN의 중심에는 합성곱 필터, 또는 커널이 있습니다. 필터는 이미지 위를 움직이는 작은 숫자판입니다. 예를 들어 3×3 필터는 이미지의 3×3 영역, 즉 9개의 픽셀을 한 번에 봅니다.

흑백 이미지의 한 부분이 다음과 같다고 해 봅시다.

이미지 일부
1 1 1
0 0 0
0 0 0

여기에 어떤 필터가 있다고 해 보겠습니다.

필터
 1  1  1
-1 -1 -1
 0  0  0

필터는 이미지 일부와 같은 위치끼리 곱한 뒤 모두 더합니다. 결과값이 크면 "내가 찾던 패턴과 비슷하다"고 볼 수 있고, 결과값이 작거나 음수이면 "그 패턴은 별로 없다"고 볼 수 있습니다.

물론 실제 CNN은 사람이 직접 "이 필터는 가로선을 찾아라", "이 필터는 모서리를 찾아라" 하고 정해 주지 않습니다. 처음에는 필터 숫자도 무작위에 가깝습니다. 모델이 예측하고, 손실을 계산하고, 역전파로 각 필터 숫자가 오차에 얼마나 영향을 주었는지 확인한 뒤, 경사하강법으로 조금씩 수정합니다.

즉 필터도 가중치입니다. 선형층의 가중치가 학습되듯, 합성곱 필터 안의 숫자들도 학습됩니다.

다만 역할이 다릅니다. 선형층의 가중치는 입력 전체를 한 번에 연결하는 경우가 많지만, 합성곱 필터는 작은 지역을 보며 같은 필터를 이미지 전체에 반복해서 적용합니다. 이 반복이 CNN의 강력한 특징입니다.

한 필터가 "왼쪽 위에 있는 세로선"만 찾는 것이 아니라, 이미지 어느 위치에 있든 비슷한 세로선을 감지할 수 있습니다. 고양이 귀가 사진의 왼쪽에 있든 오른쪽에 있든, 비슷한 모양이면 같은 필터가 반응할 수 있습니다.

특징 맵: 필터가 남긴 흔적

필터가 이미지 위를 이동하면서 각 위치마다 점수를 하나씩 계산하면, 그 결과가 다시 2차원 배열이 됩니다. 이것을 특징 맵이라고 합니다.

특징 맵은 "이 필터가 어느 위치에서 강하게 반응했는가"를 나타내는 지도입니다. 어떤 필터는 가로선에 강하게 반응할 수 있고, 어떤 필터는 둥근 곡선에 반응할 수 있습니다. 또 다른 필터는 특정 색 조합이나 질감에 반응할 수 있습니다.

초기 층의 필터는 보통 단순한 패턴을 찾습니다. 선, 모서리, 색 변화처럼 비교적 낮은 수준의 특징입니다. 층이 깊어질수록 모델은 이런 단순 특징을 조합해 더 복잡한 특징을 만듭니다. 예를 들어 첫 번째 층이 선과 모서리를 찾고, 두 번째 층이 눈이나 바퀴 같은 부분 구조를 찾고, 더 깊은 층이 고양이 얼굴이나 자동차 형태 같은 더 추상적인 패턴에 반응하는 식입니다.

이 흐름은 앞에서 배운 표현 학습과 연결됩니다. CNN은 이미지를 잘 분류하기 위해 스스로 좋은 표현을 만들어 갑니다. 다른 점은 그 표현을 만들 때 이미지의 공간 구조를 적극적으로 활용한다는 것입니다.

채널: 이미지는 한 장이 아닐 수 있다

이미지를 텐서로 다룰 때 자주 만나는 모양이 있습니다.

[채널 수, 높이, 너비]

MNIST 같은 손글씨 숫자 이미지는 보통 흑백이므로 채널이 1개입니다.

1 x 28 x 28

반면 일반적인 컬러 이미지는 빨강, 초록, 파랑 정보를 가지므로 채널이 3개입니다.

3 x 32 x 32

여기서 중요한 점은, CNN의 필터가 높이와 너비뿐 아니라 입력 채널 전체를 함께 본다는 것입니다. 예를 들어 컬러 이미지에 3×3 필터를 적용한다면, 실제 필터의 크기는 다음과 같습니다.

입력 채널 3개에 대한 3 x 3 필터
=> 3 x 3 x 3개의 가중치

PyTorch에서는 합성곱층을 다음처럼 만듭니다.

nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3)

여기서 in_channels=3은 입력 이미지가 RGB 3채널이라는 뜻입니다. out_channels=16은 필터를 16개 사용한다는 뜻입니다. 필터가 16개이면 출력 특징 맵도 16장 만들어집니다.

다시 말해 합성곱층은 이런 변환을 수행합니다.

입력:  3 x 32 x 32
출력: 16 x ? x ?

높이와 너비가 어떻게 바뀌는지는 커널 크기, stride, padding에 따라 달라집니다.

커널 크기, stride, padding

CNN을 처음 배울 때 헷갈리는 단어가 몇 가지 있습니다. 하지만 모두 "필터가 이미지를 어떻게 훑을 것인가"와 관련된 말입니다.

커널 크기는 필터가 한 번에 보는 영역의 크기입니다. kernel_size=3이면 3×3 영역을 봅니다. kernel_size=5이면 5×5 영역을 봅니다. 큰 커널은 더 넓은 주변 정보를 한 번에 볼 수 있지만, 파라미터 수가 늘어나고 계산도 많아집니다. 실습에서는 3×3 커널을 많이 사용합니다. 작지만 여러 층을 쌓으면 충분히 넓은 패턴을 단계적으로 볼 수 있기 때문입니다.

stride는 필터가 한 번에 몇 칸씩 이동하는지를 뜻합니다. stride=1이면 한 칸씩 촘촘히 움직입니다. stride=2이면 두 칸씩 건너뛰며 움직이므로 출력 특징 맵의 높이와 너비가 줄어듭니다.

padding은 이미지 가장자리에 0 같은 값을 덧대는 것입니다. 패딩이 없으면 필터가 가장자리를 충분히 덮지 못해 출력 크기가 줄어듭니다. padding=1을 주면 3×3 커널을 사용할 때 입력과 출력의 높이·너비를 유지하기 쉽습니다.

출력 크기는 다음 식으로 계산할 수 있습니다.

출력 크기 = floor((입력 크기 + 2 x padding - kernel_size) / stride) + 1

예를 들어 32×32 이미지에 3×3 커널, stride 1, padding 1을 사용하면 다음과 같습니다.

(32 + 2 x 1 - 3) / 1 + 1 = 32

그래서 높이와 너비가 유지됩니다.

conv = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1)

이 합성곱층은 다음과 같이 이미지를 바꿉니다.

3 x 32 x 32  →  16 x 32 x 32

채널 수는 3에서 16으로 늘어났습니다. 필터를 16개 사용했기 때문입니다. 높이와 너비는 32로 유지되었습니다. padding을 1로 주었기 때문입니다.

이번에는 stride를 2로 바꾸면 어떻게 될까요?

conv = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1)

출력 크기는 대략 절반으로 줄어듭니다.

3 x 32 x 32  →  16 x 16 x 16

이처럼 CNN에서는 채널, 커널 크기, stride, padding이 특징 맵의 모양을 결정합니다. 처음에는 숫자가 많아 보이지만, 질문은 하나로 정리할 수 있습니다.

필터를 몇 개 만들고, 얼마나 넓게 보고, 얼마나 촘촘히 움직이며, 가장자리를 어떻게 처리할 것인가?

풀링: 조금 움직여도 알아보는 힘

이미지 분류에서는 물체가 항상 같은 위치에 있지 않습니다. 고양이가 사진 중앙에 있을 수도 있고, 조금 왼쪽에 있을 수도 있습니다. 숫자 7도 사람마다 위치와 기울기가 조금씩 다릅니다.

CNN이 이런 작은 위치 변화에 더 강해지도록 돕는 방법 중 하나가 풀링입니다. 대표적으로 MaxPool2d가 있습니다.

nn.MaxPool2d(kernel_size=2, stride=2)

맥스 풀링은 작은 영역에서 가장 큰 값을 하나만 남깁니다. 예를 들어 2×2 영역이 다음과 같다고 해 봅시다.

0.1  0.8
0.3  0.2

맥스 풀링을 적용하면 가장 큰 값인 0.8만 남습니다.

0.8

이게 왜 도움이 될까요? 특징 맵의 어떤 근처에서 필터가 강하게 반응했다면, 정확히 어느 픽셀이었는지보다 "이 근처에 그 특징이 있다"는 사실이 더 중요할 때가 많기 때문입니다. 맥스 풀링은 위치 정보를 조금 희생하는 대신, 중요한 반응을 남기고 크기를 줄입니다.

예를 들어 눈 모양을 감지하는 필터가 있다고 해 봅시다. 눈이 한두 픽셀 오른쪽으로 이동했다고 해서 "눈이 없다"고 판단하면 곤란합니다. 풀링은 이런 작은 위치 변화에 대해 모델이 조금 더 너그러워지게 만듭니다.

또한 특징 맵의 크기를 줄이므로 계산량도 줄어듭니다.

16 x 32 x 32  →  16 x 16 x 16

합성곱층이 패턴을 찾는 역할이라면, 풀링은 "중요한 반응만 요약해서 다음 층으로 넘기는 역할"이라고 볼 수 있습니다.

CNN의 전체 흐름

일반적인 이미지 분류 CNN은 다음과 같은 흐름을 가집니다.

이미지
→ 합성곱층
→ 활성화 함수
→ 풀링
→ 합성곱층
→ 활성화 함수
→ 풀링
→ 펼치기
→ 선형층
→ 클래스 점수

앞부분의 합성곱층들은 이미지에서 특징을 추출합니다. 뒷부분의 선형층은 추출된 특징을 바탕으로 최종 분류를 수행합니다.

즉 CNN은 크게 두 부분으로 나누어 볼 수 있습니다.

특징 추출기: Conv + ReLU + Pool
분류기: Linear

이 구분은 앞으로 더 큰 모델을 볼 때도 중요합니다. 딥러닝 모델은 대개 입력을 좋은 표현으로 바꾸는 부분과, 그 표현을 사용해 예측하는 부분으로 나뉩니다. CNN에서는 합성곱층이 이미지 표현을 만들고, 선형층이 그 표현을 이용해 분류합니다.

CIFAR-10 이미지 분류기 만들기

이번에는 PyTorch로 간단한 CNN 이미지 분류기를 만들어 보겠습니다. 데이터셋은 CIFAR-10을 사용하겠습니다. CIFAR-10은 32×32 크기의 컬러 이미지로 이루어져 있으며, 10개의 클래스를 가집니다.

비행기, 자동차, 새, 고양이, 사슴,
개, 개구리, 말, 배, 트럭

MNIST보다 어렵습니다. 손글씨 숫자는 배경이 단순하고 형태도 비교적 명확하지만, CIFAR-10은 색과 배경, 물체의 모양이 훨씬 다양합니다. 그래서 이미지의 공간 구조를 활용하는 CNN의 장점을 보기 좋습니다.

먼저 필요한 라이브러리를 불러옵니다.

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

장치를 설정합니다.

device = "cuda" if torch.cuda.is_available() else "cpu"
print(device)

이미지를 텐서로 바꾸고, 값을 적당한 범위로 정규화합니다.

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(
        mean=(0.4914, 0.4822, 0.4465),
        std=(0.2470, 0.2435, 0.2616)
    )
])

여기서 ToTensor()는 이미지를 PyTorch 텐서로 바꾸며 픽셀 값을 0과 1 사이로 조정합니다. Normalize()는 채널별 평균과 표준편차를 사용해 입력 분포를 다룹니다. 정규화는 모델이 너무 큰 값이나 치우친 값 때문에 불안정하게 학습되는 일을 줄여 줍니다.

이제 데이터셋과 데이터로더를 만듭니다.

train_dataset = datasets.CIFAR10(
    root="data",
    train=True,
    download=True,
    transform=transform
)

test_dataset = datasets.CIFAR10(
    root="data",
    train=False,
    download=True,
    transform=transform
)

train_loader = DataLoader(
    train_dataset,
    batch_size=64,
    shuffle=True
)

test_loader = DataLoader(
    test_dataset,
    batch_size=64,
    shuffle=False
)

데이터 하나의 모양을 확인해 봅시다.

images, labels = next(iter(train_loader))
print(images.shape)
print(labels.shape)

출력은 보통 다음과 같습니다.

torch.Size([64, 3, 32, 32])
torch.Size([64])

배치 크기가 64이고, 각 이미지는 3채널, 높이 32, 너비 32입니다. PyTorch의 이미지 텐서는 보통 다음 순서를 사용합니다.

[배치 크기, 채널, 높이, 너비]

이제 이 입력을 받을 CNN 모델을 정의해 보겠습니다.

class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()

        self.features = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2),

            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )

        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(64 * 8 * 8, 128),
            nn.ReLU(),
            nn.Linear(128, 10)
        )

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

여기서 모양이 어떻게 바뀌는지 따라가 봅시다.

입력
[64, 3, 32, 32]

첫 번째 Conv2d(3 → 32, kernel_size=3, padding=1)
[64, 32, 32, 32]

첫 번째 MaxPool2d(2)
[64, 32, 16, 16]

두 번째 Conv2d(32 → 64, kernel_size=3, padding=1)
[64, 64, 16, 16]

두 번째 MaxPool2d(2)
[64, 64, 8, 8]

Flatten
[64, 64 * 8 * 8]

Linear
[64, 10]

마지막 출력 [64, 10]은 배치 안의 각 이미지마다 10개 클래스 점수를 만든다는 뜻입니다. 아직 확률이 아니라 점수입니다. nn.CrossEntropyLoss()는 이 점수를 받아 내부적으로 소프트맥스까지 고려해 손실을 계산합니다.

모델을 만들고 손실 함수와 옵티마이저를 준비합니다.

model = SimpleCNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

이제 학습 함수와 평가 함수를 작성합니다.

def train_one_epoch(model, loader, criterion, optimizer, device):
    model.train()
    total_loss = 0.0
    correct = 0
    total = 0

    for images, labels in loader:
        images = images.to(device)
        labels = labels.to(device)

        optimizer.zero_grad()

        outputs = model(images)
        loss = criterion(outputs, labels)

        loss.backward()
        optimizer.step()

        total_loss += loss.item() * images.size(0)
        preds = outputs.argmax(dim=1)
        correct += (preds == labels).sum().item()
        total += labels.size(0)

    avg_loss = total_loss / total
    accuracy = correct / total
    return avg_loss, accuracy

평가 함수에서는 가중치를 업데이트하지 않습니다. 그래서 torch.no_grad()를 사용합니다.

def evaluate(model, loader, criterion, device):
    model.eval()
    total_loss = 0.0
    correct = 0
    total = 0

    with torch.no_grad():
        for images, labels in loader:
            images = images.to(device)
            labels = labels.to(device)

            outputs = model(images)
            loss = criterion(outputs, labels)

            total_loss += loss.item() * images.size(0)
            preds = outputs.argmax(dim=1)
            correct += (preds == labels).sum().item()
            total += labels.size(0)

    avg_loss = total_loss / total
    accuracy = correct / total
    return avg_loss, accuracy

전체 학습 루프는 이제 익숙한 모양입니다.

epochs = 10

for epoch in range(epochs):
    train_loss, train_acc = train_one_epoch(
        model, train_loader, criterion, optimizer, device
    )
    test_loss, test_acc = evaluate(
        model, test_loader, criterion, device
    )

    print(
        f"Epoch {epoch + 1:02d} | "
        f"train loss: {train_loss:.4f}, train acc: {train_acc:.4f} | "
        f"test loss: {test_loss:.4f}, test acc: {test_acc:.4f}"
    )

학습이 잘 진행되면 손실은 대체로 내려가고 정확도는 올라갑니다. 물론 실행 환경, 초기 가중치, 학습률, 에폭 수에 따라 결과는 달라질 수 있습니다. 중요한 것은 숫자 하나를 외우는 것이 아니라, CNN이 MLP보다 이미지 구조를 더 잘 활용한다는 점을 확인하는 것입니다.

MLP와 비교하기

이번에는 같은 CIFAR-10 데이터에 MLP를 적용해 보겠습니다. MLP는 이미지를 한 줄로 펼쳐서 처리합니다.

class ImageMLP(nn.Module):
    def __init__(self):
        super().__init__()

        self.net = nn.Sequential(
            nn.Flatten(),
            nn.Linear(3 * 32 * 32, 512),
            nn.ReLU(),
            nn.Linear(512, 256),
            nn.ReLU(),
            nn.Linear(256, 10)
        )

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

같은 손실 함수와 옵티마이저를 사용해 학습할 수 있습니다.

mlp_model = ImageMLP().to(device)
mlp_criterion = nn.CrossEntropyLoss()
mlp_optimizer = torch.optim.Adam(mlp_model.parameters(), lr=0.001)

for epoch in range(epochs):
    train_loss, train_acc = train_one_epoch(
        mlp_model, train_loader, mlp_criterion, mlp_optimizer, device
    )
    test_loss, test_acc = evaluate(
        mlp_model, test_loader, mlp_criterion, device
    )

    print(
        f"Epoch {epoch + 1:02d} | "
        f"train loss: {train_loss:.4f}, train acc: {train_acc:.4f} | "
        f"test loss: {test_loss:.4f}, test acc: {test_acc:.4f}"
    )

실제로 비교해 보면, 간단한 CNN이 비슷한 규모의 MLP보다 더 좋은 성능을 내는 경우가 많습니다. 이유는 CNN이 이미지에 대해 더 알맞은 가정을 갖고 있기 때문입니다.

CNN은 가까운 픽셀들이 함께 의미 있는 패턴을 만든다고 봅니다. 또 같은 패턴이 이미지의 여러 위치에서 나타날 수 있다고 봅니다. 그래서 같은 필터를 이미지 전체에 공유해서 사용합니다. 이를 가중치 공유라고 합니다.

MLP는 입력 위치마다 별도의 가중치를 사용하는 방식에 가깝습니다. 이미지 왼쪽 위의 세로선과 오른쪽 아래의 세로선을 서로 완전히 다른 입력 조합처럼 다룰 수 있습니다. 반면 CNN의 필터는 어느 위치에서든 비슷한 패턴에 반응할 수 있습니다. 이 차이가 이미지 문제에서 큰 힘을 발휘합니다.

비유하자면 MLP는 사진을 잘게 찢어 번호를 붙인 뒤 숫자 목록으로 보는 학생에 가깝습니다. CNN은 돋보기를 들고 사진 위를 돌아다니며 "여기에는 선이 있네", "여기에는 둥근 모양이 있네", "이 조합은 바퀴 같아" 하고 관찰하는 탐정에 가깝습니다.

파라미터 수에서도 드러나는 차이

CNN의 장점은 성능뿐 아니라 파라미터 수에서도 드러납니다.

MLP의 첫 번째 선형층을 봅시다.

nn.Linear(3 * 32 * 32, 512)

입력 특성 수는 3072개입니다. 출력 뉴런은 512개입니다. 따라서 가중치만 대략 다음만큼 필요합니다.

3072 x 512 = 1,572,864개

첫 층부터 150만 개가 넘는 가중치가 생깁니다.

반면 CNN의 첫 번째 합성곱층을 봅시다.

nn.Conv2d(3, 32, kernel_size=3, padding=1)

필터 하나는 3채널 전체에 대해 3×3 크기를 가집니다.

3 x 3 x 3 = 27개

필터가 32개 있으므로 가중치는 다음과 같습니다.

27 x 32 = 864개

편향까지 포함해도 896개입니다. 차이가 매우 큽니다.

그런데 CNN은 훨씬 적은 파라미터로도 이미지의 지역 패턴을 잘 잡아낼 수 있습니다. 같은 필터를 이미지 전체에 반복 적용하기 때문입니다. 이 말은 모델이 학습해야 할 부담이 줄어든다는 뜻이기도 합니다.

물론 CNN이 항상 무조건 MLP보다 좋다는 뜻은 아닙니다. 데이터의 종류, 모델 크기, 학습 방법에 따라 결과는 달라질 수 있습니다. 하지만 이미지처럼 공간 구조가 중요한 데이터에서는 CNN이 매우 자연스럽고 강력한 선택입니다.

합성곱층 안에서 일어나는 학습

CNN을 사용한다고 해서 학습 원리가 바뀌는 것은 아닙니다. 우리가 지금까지 배운 흐름이 그대로 적용됩니다.

모델이 예측한다
→ 틀린 정도를 손실로 계산한다
→ 어느 가중치가 오차에 얼마나 영향을 줬는지 본다
→ 조금씩 수정한다
→ 반복한다

합성곱 필터도 가중치이므로 역전파를 통해 수정됩니다. 어떤 필터가 잘못된 예측에 영향을 주었다면, 그 필터의 숫자들이 손실을 줄이는 방향으로 조정됩니다. 이렇게 반복하다 보면 어떤 필터는 선에 민감해지고, 어떤 필터는 특정 색 대비에 민감해지고, 어떤 필터는 더 복잡한 부분 패턴에 반응하게 됩니다.

우리가 직접 "이 필터는 고양이 수염을 찾아라" 하고 명령하지 않았는데도, 손실을 줄이는 과정에서 유용한 패턴 감지기가 만들어집니다. 이것이 딥러닝의 재미있는 지점입니다.

모델은 정답을 맞히기 위해 필요한 표현을 스스로 찾아갑니다. CNN은 그 표현을 이미지에 맞는 방식으로, 작은 지역 패턴에서 시작해 점점 더 복잡한 구조로 쌓아 올립니다.

모델을 조금 개선해 보기

방금 만든 CNN은 아주 단순한 모델입니다. 성능을 더 높이고 싶다면 몇 가지 방향으로 개선할 수 있습니다.

  • 합성곱층을 더 깊게 쌓아 더 복잡한 특징을 학습하게 한다.

  • 채널 수를 늘려 더 다양한 필터를 사용한다.

  • 드롭아웃, 배치 정규화, 데이터 증강 같은 기법을 추가한다.

  • 학습률, 배치 크기, 에폭 수를 조정한다.

예를 들어 합성곱층을 하나 더 추가하면 모델은 더 풍부한 표현을 만들 수 있습니다.

class DeeperCNN(nn.Module):
    def __init__(self):
        super().__init__()

        self.features = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(32, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )

        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(64 * 8 * 8, 256),
            nn.ReLU(),
            nn.Linear(256, 10)
        )

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

다만 층을 깊게 쌓는다고 항상 좋아지는 것은 아닙니다. 모델이 커질수록 과적합 가능성도 커지고, 학습 시간도 늘어납니다. 그래서 좋은 딥러닝 모델을 만들려면 모델 구조뿐 아니라 정규화, 학습률, 데이터 품질, 검증 성능을 함께 살펴야 합니다. 이 주제는 뒤에서 더 자세히 다룹니다.

CNN이 보는 세계

CNN을 이해하는 좋은 방법은 "이미지를 숫자 배열로 보는 모델"에서 한 걸음 더 나아가는 것입니다. CNN은 이미지를 숫자 배열로 받지만, 그 배열 안의 가까운 숫자들이 함께 의미를 만든다고 가정합니다.

그 가정은 매우 현실적입니다. 이미지에서 픽셀 하나만 따로 떼어 보면 거의 의미가 없습니다. 하지만 픽셀들이 모이면 선이 되고, 선이 모이면 모양이 되고, 모양이 모이면 물체가 됩니다.

CNN은 바로 이 계단을 따라갑니다.

픽셀
→ 선과 모서리
→ 질감과 부분 모양
→ 물체의 특징
→ 클래스 예측

이 흐름은 나중에 Transformer를 배울 때도 중요한 배경이 됩니다. CNN은 가까운 영역의 패턴을 잘 찾도록 설계된 모델입니다. 반면 Transformer는 입력 안의 여러 위치가 서로 어떤 관계를 맺는지 더 유연하게 바라봅니다. 둘은 접근 방식이 다르지만, 공통점도 있습니다. 둘 다 원시 데이터를 더 좋은 표현으로 바꾸고, 그 표현을 바탕으로 예측합니다.

지금은 CNN을 통해 이미지 모델의 감각을 얻었습니다. 모델이 픽셀을 그대로 외우는 것이 아니라, 반복되는 패턴을 찾고, 그 패턴을 조합해 더 높은 수준의 표현을 만든다는 것을 보았습니다.

다음 장에서는 이미지가 아니라 순서가 중요한 데이터를 다룹니다. 문장, 시계열, 음성처럼 앞뒤 흐름이 의미를 만드는 데이터입니다. 이를 위해 등장하는 모델이 RNN입니다. CNN이 이미지 위를 작은 필터로 훑었다면, RNN은 시간을 따라 한 단계씩 정보를 읽어 갑니다.

책 목록

처음 만나는 딥러닝: 신경망에서 Transformer와 LLM까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기