DALL-E로 이미지를 처음 만들어봤을 때 신기하면서도 의문이 생겼습니다. AI가 "고양이"라는 단어를 입력받아 이미지를 만들어낸다는 건 알겠는데, 반대로 이미지를 보고 고양이라고 알아채는 건 어떻게 하는 걸까 싶었습니다. 픽셀 숫자 덩어리를 보고 "이건 고양이, 저건 개"라고 구분하는 게 사람한테는 당연한 일인데, 기계한테는 전혀 당연하지 않은 문제였습니다.
오늘은 이미지를 숫자로 다루는 방법부터, 합성곱 연산이 어떻게 이미지에서 패턴을 뽑아내는지, 그리고 CNN 전체 구조가 어떻게 이어지는지 실제 숫자로 따라가 보겠습니다.

이미지는 숫자다 — 픽셀과 행렬
컴퓨터가 이미지를 다루는 방식을 처음 알았을 때 생각보다 단순해서 놀랐습니다. 이미지는 그냥 숫자 행렬입니다. 흑백 이미지는 각 픽셀이 0(검정)부터 255(흰색) 사이의 값 하나를 가집니다. 28 ×28 픽셀 흑백 이미지는 784개의 숫자로 이루어진 행렬입니다.
흑백 이미지 (28×28):
각 픽셀 = 0~255 사이 숫자 1개
전체 = 28×28 = 784개 숫자로 구성된 행렬
컬러 이미지 (28×28, RGB):
각 픽셀 = R, G, B 채널 숫자 3개
전체 = 28×28×3 = 2,352개 숫자
→ (높이 × 너비 × 채널) 3차원 텐서
예시: 5×5 흑백 이미지 (숫자가 클수록 밝음)
┌────────── ┐
│ 0 0 255 255 0 │
│ 0 255 255 255 0 │
│ 0 0 255 0 0 │
│ 0 0 255 0 0 │
│ 0 0 255 0 0 │
└────────── ┘
→ 세로로 밝은 선 = 숫자 "1"처럼 보이는 이미지
일반 신경망에 이미지를 넣으면 이 행렬을 1차원으로 펼쳐서 784개 숫자를 입력으로 씁니다. 문제는 펼치는 순간 공간 정보가 사라진다는 겁니다. 위쪽 픽셀과 아래쪽 픽셀이 가까운지 먼지, 어떤 픽셀들이 모여서 선을 이루는지 — 이 정보가 전부 날아갑니다. 공간 구조를 유지하면서 패턴을 뽑아내는 방법이 필요했고, 그게 합성곱 연산입니다.
합성곱 연산 — 필터로 패턴을 뽑아내는 방법
합성곱(Convolution)은 작은 필터(커널)를 이미지 위에서 슬라이딩하면서 각 위치에서 필터와 이미지의 겹치는 부분을 원소별로 곱하고 합산하는 연산입니다. 처음엔 이게 뭔 의미인가 싶었는데, 필터 값에 따라 이미지에서 특정 패턴만 강조된다는 걸 알고 나서 이해가 됐습니다.
필터가 학습을 통해 자동으로 결정된다는 게 CNN의 핵심입니다. 사람이 "이 필터는 눈을 감지해라" 하고 직접 설계하지 않습니다. 역전파 글에서 다룬 것처럼 손실을 최소화하는 방향으로 필터 값이 스스로 조정됩니다. 결과적으로 초반 레이어는 선과 엣지를, 깊은 레이어는 눈·코·귀 같은 복잡한 패턴을 감지하는 필터가 만들어집니다.

실제 숫자로 합성곱 계산해보기
5 ×5 이미지에 3 ×3 필터를 적용하는 과정을 직접 계산해 보겠습니다. 수직 엣지를 감지하는 필터를 씁니다.
입력 이미지 (5×5): 수직 엣지 필터 (3×3):
[ 0 0 1 1 0] [-1 0 1]
[ 0 0 1 1 0] [-1 0 1]
[ 0 0 1 1 0] [-1 0 1]
[ 0 0 1 1 0]
[ 0 0 1 1 0]
→ 가운데 세로선이 있는 이미지
(0,0) 위치 계산:
이미지: 필터: 원소별 곱:
[0 0 1] [-1 0 1] [ 0 0 1]
[0 0 1] [-1 0 1] = [ 0 0 1]
[0 0 1] [-1 0 1] [ 0 0 1]
합산 = 0+0+1+0+0+1+0+0+1 = 3
(0,1) 위치 계산:
이미지: 필터: 원소별 곱:
[0 1 1] [-1 0 1] [ 0 0 1]
[0 1 1] [-1 0 1] = [ 0 0 1]
[0 1 1] [-1 0 1] [ 0 0 1]
합산 = 3
(0,2) 위치 계산:
이미지: 필터: 원소별 곱:
[1 1 0] [-1 0 1] [-1 0 0]
[1 1 0] [-1 0 1] = [-1 0 0]
[1 1 0] [-1 0 1] [-1 0 0]
합산 = -3
출력 Feature Map (3×3):
[ 3 3 -3]
[ 3 3 -3]
[ 3 3 -3]
→ 양수(3): 왼→오른쪽으로 밝아지는 경계 감지
→ 음수(-3): 오른→왼쪽으로 밝아지는 경계 감지
이 계산을 직접 해보고 나서야 "필터가 패턴을 감지한다"는 말이 이해됐습니다. 숫자가 크게 나오는 위치가 바로 그 필터가 찾는 패턴이 있는 위치입니다. 선형 회귀에서 가중치가 의미를 가졌던 것처럼, 여기서는 필터의 각 값이 "어떤 패턴에 반응할지"를 결정합니다.
풀링 — 크기를 줄이면서 특징을 유지하는 방법
합성곱 레이어 뒤에는 보통 풀링(Pooling) 레이어가 따라옵니다. Feature Map의 크기를 줄여서 연산량을 낮추고, 위치가 조금 달라져도 같은 패턴으로 인식하게 만드는 역할을 합니다. 고양이 얼굴이 이미지 왼쪽에 있든 오른쪽에 있든 고양이라고 인식해야 하는데, 풀링이 이 위치 불변성을 부여합니다.
Max Pooling 말고 Average Pooling도 있습니다. 실무에서는 Max Pooling이 더 자주 쓰입니다. 가장 강하게 반응한 위치만 남기는 게 패턴 감지에 더 유리하기 때문입니다. 입력 크기가 줄어들면서 파라미터 수도 줄어 과적합 글에서 다룬 것처럼 과적합 위험도 낮아집니다.
CNN 전체 구조 — 레이어가 쌓이면 무슨 일이 일어나는가
합성곱과 풀링이 반복되다가 마지막에 Fully Connected Layer로 이어지는 게 CNN의 기본 구조입니다. 레이어가 깊어질수록 점점 더 추상적인 패턴을 감지합니다. 처음엔 이게 어떻게 가능한지 의문이었는데, 낮은 레이어의 Feature Map이 다음 레이어의 입력이 되면서 패턴이 조합된다는 걸 알고 나서 납득이 됐습니다.
CNN 레이어별 감지 패턴 (고양이 인식 예시):
Conv Layer 1: 선, 엣지, 색상 경계
→ 가로선, 세로선, 대각선 필터들이 활성화
Conv Layer 2: 곡선, 텍스처
→ Layer 1 패턴들이 조합됨
→ 동그란 모양, 털 텍스처 감지
Conv Layer 3: 부위별 특징
→ 눈, 코, 귀 같은 복합 패턴 감지
Fully Connected Layer: 최종 분류
→ Feature Map을 1차원으로 펼침 (Flatten)
→ 일반 신경망 구조로 최종 클래스 확률 계산
→ softmax → [고양이: 0.92, 개: 0.05, 새: 0.03]
전체 구조:
입력 이미지
→ [Conv → ReLU → Pooling] × N번 반복
→ Flatten
→ [FC → ReLU] × M번
→ Softmax → 클래스 확률
Flatten 이후의 Fully Connected Layer는 앞서 다룬 일반 신경망과 구조가 완전히 같습니다. 역전파와 연쇄법칙이 FC Layer는 물론 합성곱 레이어의 필터 값까지 거슬러 올라가며 전부 업데이트합니다. CNN이 복잡해 보여도 결국 역전파 하나로 모든 파라미터가 학습되는 구조입니다.

학습 — 필터 값은 어떻게 정해지는가
CNN에서 학습해야 할 파라미터는 필터의 가중치와 FC Layer의 가중치입니다. 필터 값을 처음엔 사람이 직접 정해줘야 하는 줄 알았는데, 전부 역전파로 자동 학습됩니다. 파라미터 수를 계산해 보면 CNN이 왜 효율적인지 보입니다.
파라미터 수 비교 (32×32 이미지 기준):
일반 신경망 (FC만 사용):
입력: 32×32 = 1,024개
첫 번째 히든 레이어 128개 뉴런
파라미터 수 = 1,024 × 128 = 131,072개
CNN (3×3 필터 32개):
필터 파라미터 = 3×3×1×32 = 288개
→ 같은 Feature Map을 만드는 데 파라미터 455배 적음
가중치 공유(Weight Sharing):
하나의 필터가 이미지 전체를 슬라이딩하며 같은 가중치 사용
→ 어디서나 같은 패턴을 같은 필터로 감지
→ 파라미터 수 대폭 감소
→ 과적합 위험 감소
학습 과정:
순전파: 이미지 → 합성곱 → 풀링 → FC → 예측
손실 계산: Cross Entropy(예측, 정답)
역전파: 손실 → FC → 풀링 → 합성곱 → 필터 업데이트
가중치 공유가 CNN을 효율적으로 만드는 핵심입니다. 이미지 어디서나 같은 패턴을 같은 필터로 감지하는 게 합리적이고, 덕분에 파라미터 수가 크게 줄어듭니다. 드롭아웃, L2 규제 없이도 어느 정도 과적합을 막는 구조적 이유가 여기 있습니다.
PyTorch로 CNN 구현하기
MNIST 손글씨 숫자 분류 문제로 기본 CNN을 구현해 보겠습니다. 28 ×28 흑백 이미지를 0~9 숫자로 분류하는 모델입니다.
import torch
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
# 합성곱 블록 1
self.conv1 = nn.Conv2d(
in_channels=1, # 흑백 이미지
out_channels=32, # 필터 32개
kernel_size=3, # 3×3 필터
padding=1 # 출력 크기 유지
)
self.pool = nn.MaxPool2d(2, 2) # 2×2 풀링
# 합성곱 블록 2
self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
# Fully Connected Layer
self.fc1 = nn.Linear(64 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10) # 0~9 분류
self.relu = nn.ReLU()
def forward(self, x): # x: (batch, 1, 28, 28)
x = self.pool(self.relu(self.conv1(x))) # → (batch, 32, 14, 14)
x = self.pool(self.relu(self.conv2(x))) # → (batch, 64, 7, 7)
x = x.view(-1, 64 * 7 * 7) # Flatten
x = self.relu(self.fc1(x))
x = self.fc2(x)
return x
model = SimpleCNN()
print(model)
# 파라미터 수 확인
total = sum(p.numel() for p in model.parameters())
print(f"전체 파라미터 수: {total:,}") # → 약 421,000개
28×28 이미지가 conv1을 거치면 14 ×14로 줄고, conv2를 거치면 7 ×7이 됩니다. 풀링이 두 번 일어나면서 크기가 절반씩 줄었습니다. 처음에 이 크기 계산이 헷갈렸는데, 한 번만 직접 계산해 보면 구조가 눈에 들어옵니다. MNIST에서 이 구조로 학습하면 99% 가까운 정확도가 나옵니다. 픽셀 숫자 덩어리에서 손글씨를 거의 완벽하게 읽어내는 모델이 필터 학습 하나로 만들어지는 겁니다.
'데이터 과학 수학' 카테고리의 다른 글
| ChatGPT는 왜 오래된 정보만 아는가, RAG와 벡터 데이터베이스의 수학적 원리 (0) | 2026.05.07 |
|---|---|
| 정답 없이 데이터를 스스로 나누는 법, 군집화(K-means)와 EM 알고리즘의 수학 (0) | 2026.05.05 |
| 머신러닝의 시작, 선형 회귀(Linear Regression)와 최소제곱법·정규방정식의 수학 (0) | 2026.05.04 |
| 스팸 필터는 어떻게 작동하는가, 로지스틱 회귀(Logistic Regression)의 수학과 확률적 분류 (0) | 2026.05.03 |
| 주가와 날씨는 어떻게 예측하는가, 시계열 데이터와 LSTM의 수학적 원리 (0) | 2026.05.02 |