⊢ DeepLearning

합성곱 신경망(Convolutional Neural Network, CNN)

최 수빈 2025. 3. 20. 00:02

 

합성곱 신경망(CNN)

 

이미지 데이터를 효과적으로 처리할 수 있도록 설계된 신경망

합성곱 (Convolution)과 풀링(Pooling)을 활용하여 주요 특징을 추출

 

 

CNN의 기본 구성 요소

  • 합성곱 층(Convolutional Layer)
    입력 이미지에 필터(커널)를 적용하여 특징 맵(feature map)을 생성
    필터는 이미지의 국소적인 패턴(에지, 코너, 텍스처 등)을 학습
  • 풀링 층(Pooling Layer)
    특징 맵의 크기를 줄이고, 중요한 특징을 추출
    주로 Max Pooling과 Average Pooling이 사용됨
  • 완전 연결 층(Fully Connected Layer, FC Layer)
    추출된 특징을 바탕으로 최종 예측을 수행

 

대표적인 CNN 아키텍처

 

LeNet-5(1998년)

 

최초의 CNN 모델 중 하나

손글씨 숫자 인식 (MNIST 데이터셋 활용)

합성곱 층과 풀링 층을 반복한 후, 완전 연결 층을 사용

 

AlexNet (2012년)

 

이미지넷 대회에서 우승하며 CNN의 가능성을 입증

ReLU 활성화 함수, 드롭아웃(dropout) 사용

 

VGGNet(2014년)

 

작은 3x3 필터를 여러 층으로 쌓아 깊은 네트워크 구성 (깊고 규칙적인 구조를 가진 아키텍처)

VGG16, VGG19 모델이 대표적

 

합성곱 연산(Convolution)과 필터

 

합성곱 연산(Convolution Operation)

이미지에 필터를 적용하여 특징을 추출하는 과정

  1. 필터(커널)를 이미지의 특정 영역에 적용
  2. 필터와 해당 영역 간의 점곱(dot product) 연산 수행
  3. 결과 값을 특징 맵의 해당 위치에 저장
  4. 필터를 이동하며 전체 이미지에 대해 반복

 

필터(커널)의 역할

  • 이미지의 경계(Edge), 코너(Corner), 텍스처(Texture) 및 특정 패턴을 감지
  • 여러 개의 필터를 활용하여 다양한 특징을 추출 가능

 

풀링 레이어(Pooling)

 

특징 맵의 크기를 줄이고, 중요한 정보를 보존하는 역할을 함

 

  • 차원 축소(Dimensionality Reduction)
  • 연산량 감소 → 학습 속도 향상
  • 과적합(Overfitting) 방지

 

풀링 방식

 

Max Pooling : 특정 영역에서 최댓값 선택 (가장 두드러진 특징 유지)

Average Pooling : 특정 영역의 평균값 계산 (특징의 전체적인 정보 보존)

 

 

플래튼 레이어(Flatten Layer)

 

2차원 특징 맵을 1차원 벡터로 변환하는 역할을 함

완전 연결 층에 입력으로 사용하기 위해 필요

 

 

CNN을 이용한 이미지 분류 (PyTorch)

 

PyTorch를 활용하여 간단한 CNN 모델을 구축하고 MNIST, CIFAR-10 데이터셋을 학습

 

MNIST 데이터셋

PyTorch 및 필요한 라이브러리 임포트

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms

 

데이터셋 로드 및 전처리

# 데이터셋 전처리
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

# MNIST 데이터셋 로드
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)

testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)

 

모델 정의

# CNN 모델 정의
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, padding=1)  # 입력 채널 1, 출력 채널 32, 커널 크기 3x3, 패딩 1
        self.pool = nn.MaxPool2d(2, 2)               # 풀링 크기 2x2
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1) # 입력 채널 32, 출력 채널 64, 커널 크기 3x3, 패딩 1
        self.fc1 = nn.Linear(64 * 7 * 7, 512)        # 완전 연결 층
        self.fc2 = nn.Linear(512, 10)                # 출력 층 (10개의 클래스)

    def forward(self, x):
        x = self.pool(torch.relu(self.conv1(x))) # Conv1 → ReLU → MaxPool
        x = self.pool(torch.relu(self.conv2(x))) # Conv2 → ReLU → MaxPool
        x = x.view(x.size(0), -1) # 플래튼
        x = torch.relu(self.fc1(x)) # Fully Connected Layer + ReLU
        x = self.fc2(x) # 출력층
        return x

 

모델의 주요 연산

  • Conv2d : 합성곱 연산
    nn.Conv2d : 2차원 합성곱 층을 정의
    nn.Conv2d(in_channels, out_channels, kernel_size, padding) : 입력 채널 수, 출력 채널 수, 커널 크기, 패딩
  • MaxPool2d : 최대 풀링
    nn.MaxPool2d : 2차원 최대 풀링 층을 정의
    nn.MaxPool2d(kernel_size, stride) : 풀링 크기와 스트라이드(연산을 수행할 때 필터(커널)가 한 번에 몇 칸씩 이동하는지를 결정하는 값) 지정
  • view : 특징 맵을 1차원 벡터로 변환
  • ReLU : 활성화 함수

 

모델 학습

# 모델 초기화
model = SimpleCNN()

# 손실 함수 및 최적화 알고리즘 정의
criterion = nn.CrossEntropyLoss() # 다중 클래스 분류를 위한 손실 함수
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # SGD 최적화

# 모델 학습
for epoch in range(10): # 10 에포크 동안 학습
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        inputs, labels = data # 배치 로드

        # 기울기 초기화
        optimizer.zero_grad()

        # 순전파 + 역전파 + 최적화
        outputs = model(inputs) # 순전파
        loss = criterion(outputs, labels)
        loss.backward() # 역전파
        optimizer.step() # 최적화

        # 손실 출력
        running_loss += loss.item()
        if i % 100 == 99: # 매 100 미니배치마다 출력
            print(f'[Epoch {epoch + 1}, Batch {i + 1}] loss: {running_loss / 100:.3f}')
            running_loss = 0.0

print('Finished Training')

"""
[Epoch 1, Batch 100] loss: 1.203
[Epoch 1, Batch 200] loss: 0.260
[Epoch 1, Batch 300] loss: 0.169
[Epoch 1, Batch 400] loss: 0.149
[Epoch 1, Batch 500] loss: 0.103
[Epoch 1, Batch 600] loss: 0.093
[Epoch 1, Batch 700] loss: 0.090
[Epoch 1, Batch 800] loss: 0.071
[Epoch 1, Batch 900] loss: 0.081
[Epoch 2, Batch 100] loss: 0.062
[Epoch 2, Batch 200] loss: 0.062
[Epoch 2, Batch 300] loss: 0.054
[Epoch 2, Batch 400] loss: 0.051
[Epoch 2, Batch 500] loss: 0.054
[Epoch 2, Batch 600] loss: 0.052
[Epoch 2, Batch 700] loss: 0.043
[Epoch 2, Batch 800] loss: 0.047
[Epoch 2, Batch 900] loss: 0.046
[Epoch 3, Batch 100] loss: 0.039
[Epoch 3, Batch 200] loss: 0.035
[Epoch 3, Batch 300] loss: 0.040
[Epoch 3, Batch 400] loss: 0.037
[Epoch 3, Batch 500] loss: 0.040
[Epoch 3, Batch 600] loss: 0.027
[Epoch 3, Batch 700] loss: 0.034
[Epoch 3, Batch 800] loss: 0.047
[Epoch 3, Batch 900] loss: 0.033
[Epoch 4, Batch 100] loss: 0.028
[Epoch 4, Batch 200] loss: 0.022
[Epoch 4, Batch 300] loss: 0.030
[Epoch 4, Batch 400] loss: 0.027
[Epoch 4, Batch 500] loss: 0.032
[Epoch 4, Batch 600] loss: 0.031
[Epoch 4, Batch 700] loss: 0.021
[Epoch 4, Batch 800] loss: 0.027
[Epoch 4, Batch 900] loss: 0.029
[Epoch 5, Batch 100] loss: 0.021
[Epoch 5, Batch 200] loss: 0.019
[Epoch 5, Batch 300] loss: 0.025
[Epoch 5, Batch 400] loss: 0.018
[Epoch 5, Batch 500] loss: 0.023
[Epoch 5, Batch 600] loss: 0.013
[Epoch 5, Batch 700] loss: 0.012
[Epoch 5, Batch 800] loss: 0.022
[Epoch 5, Batch 900] loss: 0.029
[Epoch 6, Batch 100] loss: 0.017
[Epoch 6, Batch 200] loss: 0.019
[Epoch 6, Batch 300] loss: 0.017
[Epoch 6, Batch 400] loss: 0.016
[Epoch 6, Batch 500] loss: 0.018
[Epoch 6, Batch 600] loss: 0.017
[Epoch 6, Batch 700] loss: 0.021
[Epoch 6, Batch 800] loss: 0.015
[Epoch 6, Batch 900] loss: 0.014
[Epoch 7, Batch 100] loss: 0.012
[Epoch 7, Batch 200] loss: 0.012
[Epoch 7, Batch 300] loss: 0.010
[Epoch 7, Batch 400] loss: 0.013
[Epoch 7, Batch 500] loss: 0.014
[Epoch 7, Batch 600] loss: 0.013
[Epoch 7, Batch 700] loss: 0.014
[Epoch 7, Batch 800] loss: 0.012
[Epoch 7, Batch 900] loss: 0.014
[Epoch 8, Batch 100] loss: 0.008
[Epoch 8, Batch 200] loss: 0.009
[Epoch 8, Batch 300] loss: 0.012
[Epoch 8, Batch 400] loss: 0.011
[Epoch 8, Batch 500] loss: 0.007
[Epoch 8, Batch 600] loss: 0.012
[Epoch 8, Batch 700] loss: 0.013
[Epoch 8, Batch 800] loss: 0.011
[Epoch 8, Batch 900] loss: 0.008
[Epoch 9, Batch 100] loss: 0.006
[Epoch 9, Batch 200] loss: 0.007
[Epoch 9, Batch 300] loss: 0.006
[Epoch 9, Batch 400] loss: 0.008
[Epoch 9, Batch 500] loss: 0.008
[Epoch 9, Batch 600] loss: 0.008
[Epoch 9, Batch 700] loss: 0.008
[Epoch 9, Batch 800] loss: 0.006
[Epoch 9, Batch 900] loss: 0.011
[Epoch 10, Batch 100] loss: 0.006
[Epoch 10, Batch 200] loss: 0.004
[Epoch 10, Batch 300] loss: 0.006
[Epoch 10, Batch 400] loss: 0.005
[Epoch 10, Batch 500] loss: 0.005
[Epoch 10, Batch 600] loss: 0.006
[Epoch 10, Batch 700] loss: 0.008
[Epoch 10, Batch 800] loss: 0.009
[Epoch 10, Batch 900] loss: 0.004
Finished Training
"""

nn.CrossEntropyLoss

다중 클래스 분류 문제에서 주로 사용되는 손실 함수

예측 값과 실제 값 사이의 교차 엔트로피 손실 계산

optim.SGD

확률적 경사 하강법(Stochastic Gradient Descent) 최적화 알고리즘 정의

optimizer.zero_grad()

이전 단계에서 계산된 기울기 초기화

loss.backward()

역전파를 통해 기울기 계산

optimizer.step()

계산된 기울기를 바탕으로 가중치 업데이트

 

모델 평가

# 모델 평가 (테스트 데이터셋)
correct = 0
total = 0
with torch.no_grad(): # 평가 단계에서는 기울기 계산 비활성화
    for data in testloader:
        inputs, labels = data # 배치 로드
        outputs = model(inputs) # 예측 수행
        _, predicted = torch.max(outputs.data, 1) # 가장 확률이 높은 클래스 선택
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

# 정확도 출력
print(f'Accuracy of the network on the 10000 test images: {100 * correct / total:.2f}%')

"""
Accuracy of the network on the 10000 test images: 99.18%
"""

torch.no_grad()

평가 단계에서는 기울기를 계산할 필요가 없으므로, 비활성화하여 메모리 사용을 줄임

torch.max : 텐서의 최대 값을 찾음

torch.max(outputs.data, 1) : 각 샘플에 대해 가장 높은 확률을 가진 클래스를 반환

labels.size(0)
배치 크기 반환

(predicted == labels).sum().item()
예측 값과 실제 값이 일치하는 샘플의 수를 계산

 

 

CIFAR-10(Canadian Institute For Advanced Research -10 classes) 데이터셋

10개의 서로 다른 카테고리로 분류된 작은 이미지들로 구성된 데이터셋

딥러닝과 컴퓨터 비전 연구에서 가장 많이 사용되는 대표적인 데이터셋 중 하나

torchvision 라이브러리를 사용하면 간단하게 로드 가능

자연 이미지

 

크기 : 총 60,000개 이미지

  • 훈련 데이터 : 50,000개
  • 테스트 데이터 : 10,000개

이미지 크기 : 32 x 32 픽셀 (RGB, 3채널)

 

클래스(10개 카테고리)

  1. 비행기 (airplane)
  2. 자동차 (automobile)
  3. 새 (bird)
  4. 고양이 (cat)
  5. 사슴 (deer)
  6. 개 (dog)
  7. 개구리 (frog)
  8. 말 (horse)
  9. 배 (ship)
  10. 트럭 (truck)

→ 10개의 객체 분류(object classification) 문제를 풀기 위한 데이터셋

 

PyTorch 및 필요한 라이브러리 임포트

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms

 

데이터셋 로드 및 전처리

# 데이터 전처리
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

# CIFAR-10 데이터셋 다운로드 및 로드
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)

testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)

"""
100%|████████████████████████████████████████| 170M/170M [00:52<00:00, 3.25MB/s]
"""

 

간단한 CNN 모델 정의

class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        self.conv1 = nn.Conv2d(3, 32, 3, padding=1)  # 입력 채널 3, 출력 채널 32, 커널 크기 3x3
        self.pool = nn.MaxPool2d(2, 2)               # 풀링 크기 2x2
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1) # 입력 채널 32, 출력 채널 64, 커널 크기 3x3
        self.fc1 = nn.Linear(64 * 8 * 8, 512)        # 완전 연결 층
        self.fc2 = nn.Linear(512, 10)                # 출력 층 (10개의 클래스)

    def forward(self, x):
        x = self.pool(torch.relu(self.conv1(x)))
        x = self.pool(torch.relu(self.conv2(x)))
        x = x.view(-1, 64 * 8 * 8)  # 플래튼
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x

 

모델 학습

# 모델 초기화
model = SimpleCNN()

# 손실 함수 및 최적화 알고리즘
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

# 모델 학습
for epoch in range(10):  # 10 에포크 동안 학습
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        inputs, labels = data

        # 기울기 초기화
        optimizer.zero_grad()

        # 순전파 + 역전파 + 최적화
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # 손실 출력
        running_loss += loss.item()
        if i % 100 == 99:  # 매 100 미니배치마다 출력
            print(f'[Epoch {epoch + 1}, Batch {i + 1}] loss: {running_loss / 100:.3f}')
            running_loss = 0.0

print('Finished Training')

"""
[Epoch 1, Batch 100] loss: 2.116
[Epoch 1, Batch 200] loss: 1.757
[Epoch 1, Batch 300] loss: 1.555
[Epoch 1, Batch 400] loss: 1.467
[Epoch 1, Batch 500] loss: 1.406
[Epoch 1, Batch 600] loss: 1.288
[Epoch 1, Batch 700] loss: 1.298
[Epoch 2, Batch 100] loss: 1.132
[Epoch 2, Batch 200] loss: 1.126
[Epoch 2, Batch 300] loss: 1.124
[Epoch 2, Batch 400] loss: 1.047
[Epoch 2, Batch 500] loss: 1.047
[Epoch 2, Batch 600] loss: 0.986
[Epoch 2, Batch 700] loss: 1.003
[Epoch 3, Batch 100] loss: 0.905
[Epoch 3, Batch 200] loss: 0.859
[Epoch 3, Batch 300] loss: 0.872
[Epoch 3, Batch 400] loss: 0.850
[Epoch 3, Batch 500] loss: 0.850
[Epoch 3, Batch 600] loss: 0.843
[Epoch 3, Batch 700] loss: 0.815
[Epoch 4, Batch 100] loss: 0.682
[Epoch 4, Batch 200] loss: 0.709
[Epoch 4, Batch 300] loss: 0.698
[Epoch 4, Batch 400] loss: 0.702
[Epoch 4, Batch 500] loss: 0.697
[Epoch 4, Batch 600] loss: 0.692
[Epoch 4, Batch 700] loss: 0.699
[Epoch 5, Batch 100] loss: 0.535
[Epoch 5, Batch 200] loss: 0.524
[Epoch 5, Batch 300] loss: 0.546
[Epoch 5, Batch 400] loss: 0.561
[Epoch 5, Batch 500] loss: 0.527
[Epoch 5, Batch 600] loss: 0.571
[Epoch 5, Batch 700] loss: 0.554
[Epoch 6, Batch 100] loss: 0.364
[Epoch 6, Batch 200] loss: 0.352
[Epoch 6, Batch 300] loss: 0.401
[Epoch 6, Batch 400] loss: 0.395
[Epoch 6, Batch 500] loss: 0.417
[Epoch 6, Batch 600] loss: 0.411
[Epoch 6, Batch 700] loss: 0.452
[Epoch 7, Batch 100] loss: 0.239
[Epoch 7, Batch 200] loss: 0.251
[Epoch 7, Batch 300] loss: 0.256
[Epoch 7, Batch 400] loss: 0.267
[Epoch 7, Batch 500] loss: 0.290
[Epoch 7, Batch 600] loss: 0.296
[Epoch 7, Batch 700] loss: 0.298
[Epoch 8, Batch 100] loss: 0.148
[Epoch 8, Batch 200] loss: 0.145
[Epoch 8, Batch 300] loss: 0.163
[Epoch 8, Batch 400] loss: 0.180
[Epoch 8, Batch 500] loss: 0.173
[Epoch 8, Batch 600] loss: 0.189
[Epoch 8, Batch 700] loss: 0.195
[Epoch 9, Batch 100] loss: 0.095
[Epoch 9, Batch 200] loss: 0.092
[Epoch 9, Batch 300] loss: 0.092
[Epoch 9, Batch 400] loss: 0.084
[Epoch 9, Batch 500] loss: 0.103
[Epoch 9, Batch 600] loss: 0.114
[Epoch 9, Batch 700] loss: 0.120
[Epoch 10, Batch 100] loss: 0.070
[Epoch 10, Batch 200] loss: 0.057
[Epoch 10, Batch 300] loss: 0.054
[Epoch 10, Batch 400] loss: 0.047
[Epoch 10, Batch 500] loss: 0.051
[Epoch 10, Batch 600] loss: 0.086
[Epoch 10, Batch 700] loss: 0.103
Finished Training
"""

학습 과정

  • CrossEntropyLoss : 다중 클래스 분류 손실 함수
  • SGD : 확률적 경사 하강법(learning rate: 0.01, momentum: 0.9)
  • zero_grad() : 기울기 초기화
  • loss.backward() : 역전파 수행
  • optimizer.step() : 가중치 업데이트

 

모델 평가

correct = 0
total = 0
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print(f'Accuracy of the network on the 10000 test images: {100 * correct / total:.2f}%')

"""
Accuracy of the network on the 10000 test images: 73.61%
"""

 

더 깊은 CNN 아키텍처(VCG, ResNet 등)를 활용하여 더욱 정교한 모델을 만들어 볼 수도 있음