이 장에서 배울 것
PyTorch 학습 루프에서는 PyTorch로 학습 루프를 직접 만들었다. 모델을 만들고, 데이터를 넣고, 손실을 계산하고, backward()와 optimizer.step()으로 가중치를 업데이트했다. 이번 장에서는 그 학습 루프를 거의 그대로 유지한 채, 모델 구조만 이미지에 더 알맞은 형태로 바꾼다.
이 장의 목표는 CNN, 즉 합성곱 신경망을 이해하는 것이다. CNN은 이미지를 다룰 때 자주 쓰이는 신경망 구조다. 단순히 "이미지 전용 모델"이라고 외우면 부족하다. 핵심은 가까운 픽셀끼리의 관계를 보면서 작은 패턴을 찾고, 그 패턴 탐지기를 이미지 전체에 반복해서 적용한다는 점이다.
이 장을 마치면 다음을 할 수 있어야 한다.
이미지 텐서의 shape를
배치 크기 × 채널 × 높이 × 너비로 설명한다.합성곱 필터가 지역 특징을 찾는 과정을 작은 행렬 예제로 설명한다.
스트라이드, 패딩, 채널 수에 따라 feature map 크기가 어떻게 바뀌는지 계산한다.
풀링과 완전연결층의 역할을 구분한다.
MNIST에서 MLP와 CNN을 비교하고, CIFAR-10에서 데이터 증강의 효과를 확인한다.

왜 이 주제가 중요한가
이미지는 숫자의 묶음이다. 흑백 MNIST 이미지는 보통 1 × 28 × 28 크기의 텐서로 표현된다. 컬러 이미지인 CIFAR-10은 3 × 32 × 32 크기다. 여기서 3은 빨강, 초록, 파랑 채널을 뜻한다.
MLP는 이미지를 입력받을 때 보통 픽셀을 한 줄로 펼친다. 예를 들어 28 × 28 이미지는 길이 784의 벡터가 된다. 이렇게 하면 계산은 쉬워지지만 중요한 정보가 약해진다. 원래 이미지에서 서로 가까웠던 픽셀들이 벡터 안에서는 단순한 번호 차이로만 남는다. 위아래, 좌우, 모서리 같은 공간 관계를 모델이 처음부터 다시 배워야 한다.
CNN은 이 문제를 다르게 푼다. 이미지를 펼치지 않고 2차원 구조를 유지한다. 작은 필터가 이미지의 일부 영역을 훑으며 선, 모서리, 질감 같은 지역 패턴을 찾는다. 같은 필터를 이미지 여러 위치에 반복해서 적용하므로, 왼쪽 위에서 찾은 세로선 탐지기를 오른쪽 아래에서도 그대로 쓸 수 있다. 이것을 파라미터 공유라고 한다.
파라미터 공유는 두 가지 장점을 준다. 첫째, 필요한 가중치 수가 줄어든다. 둘째, 같은 패턴이 어디에 나타나도 비슷하게 감지할 수 있다. 숫자 7의 가로획이 이미지 중앙에 있든 조금 왼쪽에 있든, CNN은 같은 필터로 그 패턴을 찾을 수 있다. 이것이 CNN이 이미지에서 강한 이유다.
핵심 개념 설명
이미지 텐서와 채널
PyTorch에서 이미지는 보통 다음 shape로 다룬다.
(batch_size, channels, height, width)예를 들어 MNIST 배치 64개는 다음과 같다.
(64, 1, 28, 28)CIFAR-10 배치 64개는 다음과 같다.
(64, 3, 32, 32)채널은 이미지의 층이라고 생각하면 된다. 흑백 이미지는 밝기 정보 하나만 있으므로 채널이 1개다. 컬러 이미지는 RGB 세 가지 값이 있으므로 채널이 3개다.
합성곱 필터
합성곱은 작은 행렬인 필터를 이미지 위에 올려놓고, 겹치는 숫자끼리 곱한 뒤 모두 더하는 계산이다. 예를 들어 이미지 일부가 다음과 같다고 하자.
이미지 영역
1 1 0
1 0 0
1 1 0세로선을 찾는 필터를 다음처럼 둘 수 있다.
필터
1 0 -1
1 0 -1
1 0 -1두 행렬의 같은 위치끼리 곱해 더하면 왼쪽 값은 더해지고 오른쪽 값은 빼진다. 왼쪽이 밝고 오른쪽이 어두운 영역에서는 결과가 크게 나온다. 즉 이 필터는 "왼쪽에 밝은 세로선이 있는가?"에 반응한다.
실제 CNN은 사람이 이런 필터를 직접 설계하지 않는다. 처음에는 무작위 값으로 시작하고, 학습을 통해 필터 값이 조정된다. 앞에서 배운 손실 함수와 역전파가 여기서도 그대로 작동한다. 차이는 가중치가 완전연결층의 큰 행렬이 아니라, 작은 필터 묶음이라는 점이다.
Feature map
필터 하나를 이미지 전체에 적용하면 새로운 2차원 결과가 나온다. 이것을 feature map, 즉 특징 지도라고 한다. 어떤 위치에서 필터가 강하게 반응했는지를 나타내는 지도다.
필터가 여러 개라면 feature map도 여러 개가 나온다. 예를 들어 입력 채널이 1개인 MNIST 이미지에 16개의 필터를 적용하면 출력 채널은 16개가 된다. 각 채널은 서로 다른 패턴에 반응한다. 어떤 필터는 가로선에, 어떤 필터는 곡선에, 어떤 필터는 모서리에 반응할 수 있다.
PyTorch에서는 다음처럼 합성곱 층을 만든다.
nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3)여기서 in_channels=1은 입력 이미지가 흑백이라는 뜻이다. out_channels=16은 필터를 16개 사용한다는 뜻이다. kernel_size=3은 필터 크기가 3 × 3이라는 뜻이다.
스트라이드와 패딩
필터가 한 번에 몇 칸씩 이동하는지를 스트라이드라고 한다. stride=1이면 한 칸씩 이동한다. stride=2이면 두 칸씩 이동하므로 출력 크기가 더 작아진다.
패딩은 이미지 바깥쪽에 값을 덧붙이는 것이다. 보통 0을 둘러싼다. 패딩을 쓰면 가장자리 정보가 덜 사라지고, 출력 크기를 조절할 수 있다.
출력 높이와 너비는 다음 식으로 계산할 수 있다.
출력 크기 = floor((입력 크기 + 2 × 패딩 - 커널 크기) / 스트라이드) + 1예를 들어 28 × 28 이미지에 3 × 3 필터를 적용하고, 패딩은 0, 스트라이드는 1이라고 하자.
(28 + 2×0 - 3) / 1 + 1 = 26출력은 26 × 26이 된다.
이번에는 패딩을 1로 둔다.
(28 + 2×1 - 3) / 1 + 1 = 28출력 크기가 입력과 같은 28 × 28로 유지된다. 그래서 kernel_size=3, padding=1, stride=1 조합은 CNN에서 자주 쓰인다.
채널 수까지 포함한 shape 추적
shape를 추적할 때는 높이와 너비뿐 아니라 채널도 함께 봐야 한다.
nn.Conv2d(1, 16, kernel_size=3, padding=1)입력이 (64, 1, 28, 28)이면 출력은 다음과 같다.
(64, 16, 28, 28)배치 크기 64는 그대로다. 입력 채널 1은 출력 채널 16으로 바뀐다. 높이와 너비는 패딩 덕분에 유지된다.
다음 층이 이렇게 생겼다고 하자.
nn.Conv2d(16, 32, kernel_size=3, padding=1)입력은 (64, 16, 28, 28)이고 출력은 (64, 32, 28, 28)이다. 두 번째 합성곱 층은 앞 층이 만든 16개의 feature map을 함께 보고, 32개의 새로운 feature map을 만든다.
풀링
풀링은 feature map의 크기를 줄이는 연산이다. 대표적으로 MaxPool2d가 있다. 2 × 2 영역에서 가장 큰 값만 남긴다.
nn.MaxPool2d(kernel_size=2)28 × 28 feature map에 적용하면 보통 14 × 14가 된다. 풀링은 두 가지 역할을 한다. 첫째, 계산량을 줄인다. 둘째, 작은 위치 변화에 덜 민감하게 만든다. 예를 들어 선이 한두 픽셀 옆으로 움직여도, 주변에서 강하게 반응한 값이 남기 때문에 모델의 판단이 크게 흔들리지 않는다.
단, 풀링이 모든 정보를 보존하는 것은 아니다. 크기를 줄이는 과정에서 세부 위치 정보는 일부 사라진다. 그래서 어디서 얼마나 줄일지는 모델 설계에서 중요한 선택이다.
특징 추출과 최종 판단
CNN 모델은 크게 두 부분으로 나눌 수 있다.
첫 번째는 특징 추출기다. 합성곱, 활성화 함수, 풀링을 거치며 이미지에서 유용한 패턴을 뽑는다.
두 번째는 분류기다. 추출된 특징을 펼친 뒤 완전연결층에 넣어 최종 클래스를 판단한다.
즉 CNN은 다음 흐름으로 볼 수 있다.
이미지 → 합성곱/활성화/풀링 → 특징 → 펼치기 → 완전연결층 → 예측여기서도 학습의 큰 흐름은 바뀌지 않는다.
입력 → 출력 → 손실 → 역전파 → 업데이트바뀐 것은 입력을 이해하는 방식이다. MLP는 모든 픽셀을 한 번에 연결해 보지만, CNN은 작은 지역 패턴부터 차근차근 쌓아 올린다.
쉬운 비유 또는 사례
CNN의 필터는 작은 손전등과 비슷하다. 어두운 방 전체를 한꺼번에 보려고 하지 않고, 손전등을 조금씩 옮기며 벽의 무늬를 확인한다. 손전등이 비춘 작은 영역에서 "세로선이 있는가?", "모서리가 있는가?", "밝기가 갑자기 변하는가?"를 본다.
처음 층의 필터는 단순한 패턴을 찾는다. 선, 모서리, 색 변화 같은 것이다. 뒤쪽 층으로 갈수록 앞 층의 결과를 조합한다. 선이 모여 눈 모양이 되고, 곡선이 모여 바퀴 모양이 되며, 더 깊은 층에서는 물체의 일부나 전체 형태를 구분하는 데 도움이 되는 특징이 만들어진다.
이 과정은 사람이 이미지를 볼 때와 완전히 같지는 않다. 하지만 "작은 특징을 찾고, 그것을 조합해 더 큰 의미를 만든다"는 점에서 좋은 직관을 준다.
직접 해보는 실습
이번 실습의 목표는 두 가지다. 첫째, MNIST에서 MLP와 CNN을 비교한다. 둘째, CIFAR-10에서 데이터 증강이 일반화 성능에 어떤 영향을 주는지 확인한다.
실습을 할 때는 가능하면 train/validation/test를 구분한다. 모델 구조나 하이퍼파라미터를 고를 때는 validation set을 사용하고, test set은 마지막 확인에 남겨 둔다.
실습 1: MNIST CNN 만들기
아래 모델은 간단한 MNIST 분류용 CNN이다.
import torch
import torch.nn as nn
import torch.nn.functional as F
class MNISTCNN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, padding=1), # 1x28x28 -> 16x28x28
nn.ReLU(),
nn.MaxPool2d(2), # 16x14x14
nn.Conv2d(16, 32, kernel_size=3, padding=1), # 32x14x14
nn.ReLU(),
nn.MaxPool2d(2) # 32x7x7
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(32 * 7 * 7, 10)
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x이 모델의 shape 흐름을 직접 써 보자.
입력: (batch, 1, 28, 28)
Conv1: (batch, 16, 28, 28)
MaxPool1: (batch, 16, 14, 14)
Conv2: (batch, 32, 14, 14)
MaxPool2: (batch, 32, 7, 7)
Flatten: (batch, 32*7*7)
Linear: (batch, 10)학습 루프는 PyTorch 학습 루프에서 만든 흐름과 거의 같다. 아래 예시는 검증 데이터를 사용해 학습 중 성능을 확인한다.
model = MNISTCNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for epoch in range(5):
model.train()
total_loss = 0
for x, y in train_loader:
x, y = x.to(device), y.to(device)
optimizer.zero_grad()
logits = model(x)
loss = criterion(logits, y)
loss.backward()
optimizer.step()
total_loss += loss.item()
model.eval()
correct = 0
total = 0
with torch.no_grad():
for x, y in val_loader:
x, y = x.to(device), y.to(device)
logits = model(x)
pred = logits.argmax(dim=1)
correct += (pred == y).sum().item()
total += y.size(0)
print(epoch, total_loss / len(train_loader), correct / total)비교를 위해 같은 데이터에 MLP도 학습해 보자. MLP는 입력을 784개 숫자로 펼친다. CNN은 1 × 28 × 28 구조를 유지한다. 두 모델의 검증 정확도뿐 아니라, 틀린 이미지를 몇 개 시각화해 보면 차이를 더 잘 이해할 수 있다. 모든 모델 선택이 끝난 뒤에는 test set에서 마지막 평가를 한 번 수행한다.
실습 2: CIFAR-10에서 데이터 증강 확인하기
CIFAR-10은 MNIST보다 어렵다. 흑백 숫자가 아니라 컬러 물체 이미지다. 같은 고양이라도 위치, 배경, 색, 각도가 다르다. 이때 데이터 증강이 도움이 된다.
데이터 증강은 학습 이미지를 조금씩 변형해 모델이 다양한 상황을 보게 만드는 방법이다. 예를 들어 이미지를 좌우로 뒤집거나, 일부를 잘라내거나, 색을 조금 바꿀 수 있다.
from torchvision import transforms
train_transform = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
])
val_test_transform = transforms.Compose([
transforms.ToTensor(),
])중요한 점은 validation과 test 데이터에는 무작위 증강을 적용하지 않는다는 것이다. 학습 데이터에는 변형을 주어도 되지만, 검증과 테스트는 정해진 기준으로 평가해야 한다.
실습 보고서에는 다음을 적어 보자.
MLP와 CNN의 MNIST 검증/테스트 정확도 차이
두 모델이 자주 틀린 사례
CIFAR-10에서 증강을 사용했을 때와 사용하지 않았을 때의 차이
훈련 정확도와 검증 정확도의 간격
CNN이 이미지 구조를 활용한다는 점을 실험 결과와 연결한 해석
흔한 실수와 오류 분석
입력 shape를 잘못 넣는 경우
PyTorch의 Conv2d는 입력을 (batch, channels, height, width)로 기대한다. 그런데 이미지를 (batch, height, width, channels) 형태로 넣으면 오류가 나거나 잘못 학습된다. 특히 다른 라이브러리에서 가져온 이미지 배열은 채널이 마지막에 있는 경우가 많다. 이때는 차원 순서를 바꿔야 한다.
x = x.permute(0, 3, 1, 2)Flatten 크기를 잘못 계산하는 경우
CNN에서 가장 자주 나는 오류 중 하나는 Linear 층의 입력 크기를 잘못 쓰는 것이다.
mat1 and mat2 shapes cannot be multiplied이런 오류가 나오면 합성곱과 풀링을 지난 뒤의 shape를 다시 계산해야 한다. 가장 쉬운 방법은 forward() 안에서 잠시 print(x.shape)를 넣어 확인하는 것이다.
CrossEntropyLoss 앞에 softmax를 붙이는 경우
PyTorch의 nn.CrossEntropyLoss()는 내부에서 softmax에 해당하는 계산을 포함한다. 따라서 모델 마지막에 Softmax를 붙이지 않는 것이 일반적이다. 모델은 점수인 logits를 출력하고, 손실 함수가 그 값을 받아 처리한다.
train 모드와 eval 모드를 구분하지 않는 경우
CNN 자체만 사용할 때는 차이가 작게 느껴질 수 있다. 하지만 Dropout이나 BatchNorm을 쓰면 model.train()과 model.eval()의 차이가 중요하다. 학습할 때는 model.train(), 평가할 때는 model.eval()을 호출하는 습관을 유지하자.
CNN을 무조건 깊게 만드는 경우
층을 많이 쌓는다고 항상 좋아지는 것은 아니다. 데이터가 작거나 학습 설정이 맞지 않으면 더 깊은 모델이 오히려 불안정해질 수 있다. 처음에는 작은 CNN으로 시작하고, 학습 곡선을 보며 깊이와 채널 수를 늘리는 것이 좋다.
test set으로 계속 비교하는 경우
CNN과 MLP를 비교할 때 test set을 반복해서 보면, 어느새 test set에 맞는 선택을 하게 된다. 실험 중 비교는 validation set으로 하고, test set은 최종 모델이 정해진 뒤 한 번 확인한다.
요약
CNN은 이미지의 공간 구조를 활용하는 신경망이다. MLP처럼 픽셀을 모두 펼쳐서 보는 대신, 작은 필터를 이미지 위에 반복 적용한다. 이 필터는 지역 패턴을 찾고, 학습을 통해 더 유용한 값으로 조정된다.
스트라이드는 필터가 이동하는 간격이다. 패딩은 가장자리 주변에 값을 덧붙여 출력 크기를 조절한다. 풀링은 feature map의 크기를 줄이고 작은 위치 변화에 덜 민감하게 만든다. 여러 합성곱 층을 거치면 단순한 선과 모서리에서 더 복잡한 특징으로 표현이 발전한다.
CNN 모델은 보통 특징 추출기와 분류기로 나뉜다. 특징 추출기는 합성곱, 활성화 함수, 풀링으로 구성된다. 분류기는 추출된 특징을 바탕으로 최종 클래스를 판단한다. 학습 루프는 이전 장과 같다. 달라지는 것은 모델이 이미지를 바라보는 방식이다.
퀴즈
MLP가 이미지를 한 줄로 펼칠 때 잃기 쉬운 정보는 무엇인가?
Conv2d(1, 16, kernel_size=3, padding=1)에(32, 1, 28, 28)입력을 넣으면 출력 shape는 무엇인가?28 × 28입력에kernel_size=3,padding=0,stride=1합성곱을 적용하면 출력 크기는 얼마인가?풀링은 왜 사용하는가? 장점과 잃을 수 있는 정보를 함께 설명해 보라.
CrossEntropyLoss를 사용할 때 모델 마지막에Softmax를 보통 넣지 않는 이유는 무엇인가?CNN의 파라미터 공유가 이미지 분류에 도움이 되는 이유를 설명해 보라.
validation set과 test set을 구분해야 하는 이유는 무엇인가?
체크리스트
이미지 텐서를
(batch, channels, height, width)순서로 설명할 수 있다.합성곱 필터가 작은 영역에서 곱하고 더하는 계산임을 설명할 수 있다.
스트라이드와 패딩이 출력 크기에 미치는 영향을 계산할 수 있다.
feature map의 의미를 설명할 수 있다.
풀링과 완전연결층의 역할을 구분할 수 있다.
MNIST용 간단한 CNN을 PyTorch로 구현할 수 있다.
MLP와 CNN의 차이를 실험 결과와 연결해 설명할 수 있다.
CIFAR-10에서 학습 데이터 증강과 검증/테스트 변환을 구분할 수 있다.
다음 장 연결
이번 장에서는 이미지처럼 공간 구조가 있는 데이터를 다루었다. CNN은 가까운 픽셀 사이의 관계를 활용해 지역 패턴을 찾는다. 다음 장에서는 순서가 중요한 데이터를 다룬다. 문장, 음성, 주가, 센서 기록처럼 시간이나 순서가 의미를 가지는 데이터다.
이미지에서는 "가까운 픽셀끼리의 관계"가 중요했다. 시퀀스에서는 "앞뒤 항목 사이의 관계"가 중요하다. 다음 장에서는 RNN 계열 모델의 기본 아이디어를 살펴보고, Attention이 왜 등장했는지 이해한다. CNN에서 필터가 중요한 위치를 찾아냈다면, Attention은 입력들 사이에서 어떤 부분을 더 주의 깊게 볼지 학습한다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.