CNN(Convolutional Neural Network)概述
CNN(Convolutional Neural Network, 합성곱 신경망)은 딥러닝의 핵심 모델 중 하나로, 이미지 인식, 영상 분석, 자연어 처리 등 다양한 분야에서 활발하게 활용되고 있습니다. 인간의 시각 시스템 동작 원리를 모방하여 이미지 또는 기타 데이터에서 자동으로 특성을 추출함으로써 효율적인 분류, 탐지, 생성 작업을 수행할 수 있습니다.
1. CNN의 탄생과 발전
1.1 탄생 배경
- 생물학적 영감: 1960년대 Hubel과 Wiesel이 고양이的大脑피질을 연구하면서, 시각 정보가大脑에서 계층적으로 처리된다는 사실을 발견했습니다. 이 발견은 CNN 설계의 이론적 기초가 되었습니다.
- 최초의 CNN - LeNet-5: 1998년 Yann LeCun 등이 LeNet-5를 제안했습니다. 이 모델은 필기 숫자 인식(MNIST 데이터셋)에 사용되었으며, CNN이 이미지 인식 작업에서 큰 잠재력을 가지고 있음을 입증했습니다.
1.2 발전 과정
2012년 AlexNet이 ImageNet 대회지면서 획기적인 성적을 거두며 딥러닝 시대의 문을 열었습니다. 이후 VGGNet, GoogLeNet(Inception 시리즈), ResNet 등 더욱 진보된 CNN 구조가 등장하며 CNN의 발전이 가속화되었습니다. 이러한 구조들은 모델 깊이, 파라미터 수, 계산 효율성 측면에서 지속적으로 최적화되어 왔습니다.
2. CNN의 기본 구조
2.1 입력층(Input Layer)
입력층은 원본 데이터를 받습니다. 이미지의 경우 픽셀값을 입력받으며, 컬러 이미지의 경우 높이, 너비, 채널(RGB 3개 채널)로 구성된 3차원 데이터입니다.
2.2 합성곱층(Convolutional Layer)
합성곱층은 CNN의 핵심으로, 입력 데이터의 국부 특성을 추출합니다. 합성곱 커널(필터)이 입력 데이터 위를 슬라이딩하며 합성곱 연산을 수행합니다.
- 커널 크기는 일반적으로 입력 데이터보다 작으며 3×3 또는 5×5를 사용합니다.
- 커널의 가중치는 훈련 과정에서 계속 업데이트되어 더 유용한 특성을 학습합니다.
- 합성곱 연산 공식: 출력 = 입력 × 커널 + 바이어스
- 각 커널은 서로 다른 특성을 추출하여 여러 개의 특성 맵(Feature Map)을 생성합니다.
2.3 활성화층(Activation Layer)
활성화층은 비선형성을 도입하여 CNN이 더 복잡한 특성을 학습할 수 있게 합니다. 대표적인 활성화 함수:
- ReLU(Rectified Linear Unit): f(x) = max(0, x) - 가장 널리 사용되며 계산이 간단하고 그래디언트 소실 문제를 완화합니다.
- Sigmoid: 0~1 사이의 값 출력
- Tanh: -1~1 사이의 값 출력
2.4 풀링층(Pooling Layer)
풀링층은 특성 맵의 공간 차원을 축소하여 계산량과 파라미터 수를 줄이면서 중요한 특성을 보존합니다.
- 최대 풀링(Max Pooling): 풀링 윈도우 내 최대값 선택
- 평균 풀링(Average Pooling): 풀링 윈도우 내 평균값 계산
2.5 전결합층(Fully Connected Layer)
전결합층은 합성곱층과 풀링층에서 추출된 특성을 통합하여 최종 분류 또는 회귀 결과를 출력합니다. 분류 작업에는 Softmax, 회귀 작업에는 선형 함수를 사용합니다.
2.6 출력층(Output Layer)
작업 유형에 따라 최종 결과를 출력합니다. 분류 작업의 경우 클래스 수만큼 뉴런을 두고, 각 뉴런의 출력값은 해당 클래스의 확률을 나타냅니다.
3. CNN의 동작 원리
3.1 순전파(Forward Propagation)
- 입력 데이터가 입력층을 통과합니다.
- 합성곱층에서 커널이 슬라이딩하며 합성곱 연산 수행, 특성 맵 생성
- 활성화층에서 비선형 변환 적용
- 풀링층에서 공간 차원 축소
- 여러 합성곱-활성화-풀링層 통과 후, 특성 맵을 1차원 벡터로 평탄화
- 전결합층에서 최종 분류 또는 회귀 결과 출력
3.2 역전파(Backward Propagation)
- 출력층에서 손실값 계산(분류: 교차 엔트로피, 회귀: 평균 제곱 오차)
- 체인룰을 적용하여 손실값을 각 층으로 역전파
- 각 층의 그래디언트 계산
- 옵티마이저(SGD, Adam 등)를 사용하여 파라미터 업데이트
4. CNN 구현 예시: MNIST 필기 숫자 인식
이제 PyTorch를 사용하여 MNIST 필기 숫자 인식 CNN을 구현하겠습니다.
4.1 라이브러리 임포트 및 설정
import torch
import torch.nn as neural_network
import torch.utils.data as data_utils
import torchvision
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
- torch: 텐서 연산 및 자동 미분을 위한 핵심 라이브러리
- neural_network(nn): 신경망 구성 요소(층, 모듈 등)
- data_utils: 배치 데이터 로딩 유틸리티
- torchvision: 컴퓨터 비전용 데이터셋 및 변환 도구
- matplotlib: 결과 시각화(Agg 백엔드로 비GUI 환경 지원)
4.2 하이퍼파라미터 설정
training_epochs = 3
batch_size = 64
learning_rate = 0.001
download_data = True
- training_epochs: 전체 데이터셋을 학습시키는 횟수
- batch_size: 한 번에 처리하는 샘플 수
- learning_rate: 파라미터 업데이트의 보폭 크기
- download_data: MNIST 데이터셋 다운로드 여부
4.3 데이터 준비
training_dataset = torchvision.datasets.MNIST(
root='./mnist_data',
train=True,
transform=torchvision.transforms.ToTensor(),
download=download_data
)
print(f"학습 데이터 크기: {training_dataset.data.shape}")
print(f"학습 레이블 크기: {training_dataset.targets.shape}")
training_loader = data_utils.DataLoader(
dataset=training_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=2
)
test_dataset = torchvision.datasets.MNIST(
root='./mnist_data',
train=False,
transform=torchvision.transforms.ToTensor()
)
test_samples = test_dataset.data.unsqueeze(1).float()[:2000] / 255.0
test_labels = test_dataset.targets[:2000]
- ToTensor(): PIL 이미지를 [0,1] 범위의 float 텐서로 변환
- unsqueeze(1): 채널 차원 추가 (기존 [N, 28, 28] → [N, 1, 28, 28])
- / 255.0: 픽셀값 정규화
4.4 CNN 모델 정의
class ConvNet(neural_network.Module):
def __init__(self):
super(ConvNet, self).__init__()
# 첫 번째 합성곱 블록
self.conv_block1 = neural_network.Sequential(
neural_network.Conv2d(
in_channels=1,
out_channels=32,
kernel_size=3,
padding=1
),
neural_network.ReLU(),
neural_network.MaxPool2d(kernel_size=2, stride=2)
)
# 두 번째 합성곱 블록
self.conv_block2 = neural_network.Sequential(
neural_network.Conv2d(
in_channels=32,
out_channels=64,
kernel_size=3,
padding=1
),
neural_network.ReLU(),
neural_network.MaxPool2d(kernel_size=2, stride=2)
)
# 세 번째 합성곱 블록
self.conv_block3 = neural_network.Sequential(
neural_network.Conv2d(
in_channels=64,
out_channels=128,
kernel_size=3,
padding=1
),
neural_network.ReLU(),
neural_network.MaxPool2d(kernel_size=2, stride=2)
)
# 전결합층
self.classifier = neural_network.Sequential(
neural_network.Flatten(),
neural_network.Linear(128 * 3 * 3, 256),
neural_network.ReLU(),
neural_network.Dropout(0.5),
neural_network.Linear(256, 10)
)
def forward(self, x):
x = self.conv_block1(x)
x = self.conv_block2(x)
x = self.conv_block3(x)
x = self.classifier(x)
return x
model = ConvNet()
print("모델 구조:")
print(model)
모델 구조 설명:
- 3개의 합성곱 블록: 각각 합성곱 → ReLU → 최대 풀링 구성
- 채널 변화: 1 → 32 → 64 → 128
- 공간 크기 변화: 28×28 → 14×14 → 7×7 → 3×3
- 전결합층: 3×3×128 = 1152개 특성 → 256 → 10개 클래스
- Dropout(0.5): 과적합 방지
4.5 학습 환경 설정
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
loss_function = neural_network.CrossEntropyLoss()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
print(f"사용 장치: {device}")
- Adam 옵티마이저: 학습률 자동 조절
- CrossEntropyLoss: 다중 분류용 손실 함수
- GPU 사용 가능시 CUDA로 모델 이동
4.6 학습 루프
def train_model(epochs):
training_history = []
for epoch in range(epochs):
model.train()
total_loss = 0
correct_predictions = 0
total_samples = 0
for batch_idx, (images, labels) in enumerate(training_loader):
images = images.to(device)
labels = labels.to(device)
# 순전파
predictions = model(images)
loss = loss_function(predictions, labels)
# 역전파
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
_, predicted = torch.max(predictions.data, 1)
total_samples += labels.size(0)
correct_predictions += (predicted == labels).sum().item()
if batch_idx % 100 == 0:
print(f"Epoch [{epoch+1}/{epochs}] Batch [{batch_idx}/{len(training_loader)}] Loss: {loss.item():.4f}")
avg_loss = total_loss / len(training_loader)
accuracy = 100.0 * correct_predictions / total_samples
training_history.append({'epoch': epoch+1, 'loss': avg_loss, 'accuracy': accuracy})
# 검증
model.eval()
with torch.no_grad():
test_images = test_samples.to(device)
test_preds = model(test_images)
_, test_predicted = torch.max(test_preds.data, 1)
test_accuracy = 100.0 * (test_predicted.cpu() == test_labels).sum().item() / test_labels.size(0)
print(f"Epoch [{epoch+1}/{epochs}] - Train Loss: {avg_loss:.4f}, Train Acc: {accuracy:.2f}%, Test Acc: {test_accuracy:.2f}%")
return training_history
history = train_model(training_epochs)
4.7 예측 결과 확인
model.eval()
with torch.no_grad():
sample_images = test_samples[:15].to(device)
sample_predictions = model(sample_images)
_, predicted_classes = torch.max(sample_predictions.data, 1)
print("=" * 50)
print("예측 결과:")
print(predicted_classes.cpu().numpy())
print("실제 레이블:")
print(test_labels.numpy()[:15])
print("=" * 50)
4.8 전체 코드
import torch
import torch.nn as neural_network
import torch.utils.data as data_utils
import torchvision
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
# 하이퍼파라미터
training_epochs = 3
batch_size = 64
learning_rate = 0.001
download_data = True
# 데이터 로드
training_dataset = torchvision.datasets.MNIST(
root='./mnist_data',
train=True,
transform=torchvision.transforms.ToTensor(),
download=download_data
)
print(f"학습 데이터 크기: {training_dataset.data.shape}")
print(f"학습 레이블 크기: {training_dataset.targets.shape}")
training_loader = data_utils.DataLoader(
dataset=training_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=2
)
test_dataset = torchvision.datasets.MNIST(
root='./mnist_data',
train=False,
transform=torchvision.transforms.ToTensor()
)
test_samples = test_dataset.data.unsqueeze(1).float()[:2000] / 255.0
test_labels = test_dataset.targets[:2000]
# 모델 정의
class ConvNet(neural_network.Module):
def __init__(self):
super(ConvNet, self).__init__()
self.conv_block1 = neural_network.Sequential(
neural_network.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1),
neural_network.ReLU(),
neural_network.MaxPool2d(kernel_size=2, stride=2)
)
self.conv_block2 = neural_network.Sequential(
neural_network.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1),
neural_network.ReLU(),
neural_network.MaxPool2d(kernel_size=2, stride=2)
)
self.conv_block3 = neural_network.Sequential(
neural_network.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1),
neural_network.ReLU(),
neural_network.MaxPool2d(kernel_size=2, stride=2)
)
self.classifier = neural_network.Sequential(
neural_network.Flatten(),
neural_network.Linear(128 * 3 * 3, 256),
neural_network.ReLU(),
neural_network.Dropout(0.5),
neural_network.Linear(256, 10)
)
def forward(self, x):
x = self.conv_block1(x)
x = self.conv_block2(x)
x = self.conv_block3(x)
x = self.classifier(x)
return x
model = ConvNet()
print("모델 구조:")
print(model)
# 학습 환경
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
loss_function = neural_network.CrossEntropyLoss()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
print(f"사용 장치: {device}")
# 학습 루프
def train_model(epochs):
for epoch in range(epochs):
model.train()
total_loss = 0
for batch_idx, (images, labels) in enumerate(training_loader):
images = images.to(device)
labels = labels.to(device)
predictions = model(images)
loss = loss_function(predictions, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
if batch_idx % 100 == 0:
print(f"Epoch [{epoch+1}/{epochs}] Batch [{batch_idx}/{len(training_loader)}] Loss: {loss.item():.4f}")
avg_loss = total_loss / len(training_loader)
model.eval()
with torch.no_grad():
test_images = test_samples.to(device)
test_preds = model(test_images)
_, test_predicted = torch.max(test_preds.data, 1)
test_accuracy = 100.0 * (test_predicted.cpu() == test_labels).sum().item() / test_labels.size(0)
print(f"Epoch [{epoch+1}/{epochs}] - Loss: {avg_loss:.4f}, Test Acc: {test_accuracy:.2f}%")
train_model(training_epochs)
# 예측 결과
model.eval()
with torch.no_grad():
sample_images = test_samples[:15].to(device)
sample_predictions = model(sample_images)
_, predicted_classes = torch.max(sample_predictions.data, 1)
print("=" * 50)
print("예측 결과:", predicted_classes.cpu().numpy().tolist())
print("실제 레이블:", test_labels.numpy()[:15].tolist())
print("=" * 50)
5. 결론
본 가이드에서는 CNN의 기본 구조와 동작 원리를 살펴보고, PyTorch를 활용한 MNIST 필기 숫자 인식 구현을 진행했습니다. 핵심 내용 정리:
- 합성곱층: 지역적 특성 추출, 가중치 공유로 효율적인 파라미터 사용
- 활성화층: 비선형성 추가를 통한 복잡한 패턴 학습
- 풀링층: 공간 차원 축소로 계산 효율성 향상
- 전결합층: 특성 통합 후 최종 분류
- 역전파: 손실값 기반 파라미터 자동 최적화
CNN은 计算机 비전 분야에서 가장 기본적이고 강력한 도구로, 더 복잡한 작업(R-CNN, YOLO, U-Net 등)으로 확장할 수 있는 근간이 됩니다.