합성곱 신경망은 주로 이미지 처리 및 컴퓨터 비전 작업을 위한 인공신경망으로, 합성곱(Convolution) 연산을 통해 공간적 구조 정보를 효율적으로 학습할 수 있는 신경망이다. 최근에는 추천 시스템, 자연어 처리, 시계열 분석 등 다른 분야에도 합성곱 신경망이 도입되고 있다.
합성곱 신경망은 동물의 시각피질(Visual Cortex) 연구에서 영감을 받아 개발되었다. 시각피질은 동물의 뇌에서 시각 정보를 처리하는 부분인데, 이 영역의 뉴런들은 특정한 시각 자극, 특히 이미지의 특정 위치에 있는 패턴(모서리, 방향 등)에 반응한다. 이를 수용장(Receptive Field) 이라고 한다. 이러한 뉴런들이 이미지의 작은 영역을 처리하고, 그 정보가 결합되어 더 복잡한 시각 패턴을 인식할 수 있다.
합성곱 신경망은 시각피질이 이미지의 특정 패턴을 인식하는 방식에서 착안하여 합성곱 연산을 통해 이미지의 특징을 추출하고 이를 기반으로 다양한 시각적 작업을 수행할 수 있도록 만들어졌다.
CNN 모델 실습을 위한 다양한 이미지 데이터셋이 공개되어 있다. 대표적인 공개 이미지 데이터셋으로는 사람이 손으로 쓴 숫자 이미지를 분류하기 위한 MNIST, 의류 이미지 분류를 위한 FashionMNIST, 음식 이미지 분류를 위한 Food-101 등이 있다.
이 글에서는 파이토치로 세 가지 음식(스시, 스테이크, 피자) 이미지를 분류하는 간단한 CNN 모델을 만들려고 한다. 다음은 실습에 필요한 데이터셋(Food-101의 일부)을 다운로드하는 코드이다.
import os
import requests
import zipfile
from pathlib import Path
import torch
from torch import nn
data_path = Path("data/")
image_path = data_path / "pizza_steak_sushi"
if image_path.is_dir():
print(f"{image_path} directory already exists, skipping download")
else:
print(f"{image_path} directory does not exist, creating one")
image_path.mkdir(parents=True, exist_ok=True)
with open(data_path / "pizza_steak_sushi.zip", "wb") as f:
request = requests.get("https://github.com/mrdbourke/pytorch-deep-learning/raw/main/data/pizza_steak_sushi.zip")
print("downloading data...")
f.write(request.content)
# 압축 해제
with zipfile.ZipFile(data_path / "pizza_steak_sushi.zip", "r") as zip_ref:
print("unzipping dataset...")
zip_ref.extractall(image_path)
train_dir = image_path / "train"
test_dir = image_path / "test"
일반적으로 컬러 이미지는 빛의 3원색(빨강, 초록, 파랑)으로 나타낸다. 이 세가지 색의 빛을 0~255의 강도로 나타내고, 각각의 색 정보를 조합하여 여러 색을 표현할 수 있다. 따라서 컬러 이미지 데이터는 RGB의 3채널에 대해 이미지의 크기(가로 X 세로) 만큼의 색 정보를 담는 텐서로 변환하여 활용할 수 있다.
예를 들면 가로가 500px, 세로가 300px인 컬러 이미지 텐서의 크기는 3x500x300 또는 500x300x3이 된다. 전자의 경우 CxHxW 형식이라고 하며 후자는 HxWxC 형식이라고 한다. 파이썬의 시각화 라이브러리인 matplotlib
의 imshow
메서드로 이미지를 시각화 하려면 HxWxC 형식만 가능하기 때문에 permute
등의 메서드로 텐서의 형식을 변환해야한다.
그레이스케일 이미지의 경우 밝기 정보만으로 이미지를 표현할 수 있으므로 색 채널은 1채널이 되어 1xHxW 또는 HxWx1 형식의 텐서로 표현할 수 있다.
파이토치의 torchvision.transforms
모듈에서는 리사이즈나 텐서 변환 등을 포함하는 다양한 이미지 변환 기능들을 제공한다. 또한 transforms 모듈의 Compose
로 입력 이미지를 정의한 순서대로 변환할 수 있다.
파이토치의 데이터셋 모듈은 데이터셋을 구성하는 작업을 위한 기능들을 제공한다. 이미지 데이터셋을 디렉터리 기준으로 불러올 수 있는 torchvision.datasets.ImageFolder
를 이용하면 편하게 데이터셋을 구성할 수 있다.
ImageFolder
는 데이터셋의 최하위 디렉토리가 클래스명인 경우 자동으로 해당 파일의 클래스를 구성해준다. 예를 들어 root/cat/123.png
라는 파일이 있다면 이 123.png이라는 이미지 파일의 클래스는 cat이 된다.
파이토치의 DataLoader
는 데이터를 효율적으로 로드하고, 모델 학습을 위해 배치(batch) 단위로 데이터를 제공한다. DataLoader는 주로 대규모 데이터셋을 처리할 때 유용하며, 데이터의 셔플링, 배치 크기 조정, 병렬 데이터 로딩 등의 편리한 기능을 지원한다. DataLoader를 이용하면 CNN 모델의 미니 배치 학습을 편리하게 수행할 수 있다.
다음은 파이토치의 Compose
, ImageFolder
, DataLoader
를 이용하여 이미지 데이터셋을 준비하는 코드이다.
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
data_transform = transforms.Compose([
transforms.Resize(size=(64, 64)), # 이미지를 64x64 크기로 리사이즈
transforms.RandomHorizontalFlip(p=0.5), # 이미지를 수평축으로 뒤집음
transforms.ToTensor() # PIL 이미지 객체 또는 넘파이 배열을 파이토치 텐서로 변환
])
train_data = datasets.ImageFolder(
root=train_dir, # 데이터가 있는 루트 디렉터리 지정
transform=data_transform, # 데이터 변환기 지정
)
test_data = datasets.ImageFolder(
root=test_dir,
transform=data_transform,
)
BATCH_SIZE = 32
NUM_WORKERS = os.cpu_count() # os.cpu_count(): 시스템의 CPU 코어 수를 반환
train_dataloader = DataLoader(
dataset=train_data,
batch_size=BATCH_SIZE,
num_workers=NUM_WORKERS, # 모든 CPU 코어 사용
shuffle=True
)
test_dataloader = DataLoader(
dataset=test_data,
batch_size=BATCH_SIZE,
num_workers=NUM_WORKERS,
shuffle=False
)
데이터 증강은 모델의 일반화 성능을 높이기 위해 학습 데이터셋을 인위적으로 확장하는 기법으로 이미지, 텍스트, 오디오 등의 데이터 학습을 위해 유용하게 사용된다. 데이터 증강은 원본 데이터를 변형하여 새로운 데이터를 생성함으로써 모델이 다양한 패턴을 학습하도록 돕는다.
데이터 증강을 통해 모델이 다양한 데이터 패턴을 학습하게 되어, 과적합(overfitting)을 방지하고 모델의 일반화 성능을 높일 수 있다. 또한 데이터셋이 충분하지 않을 때에도 증강 기법을 통해 학습 데이터의 양을 늘려 모델의 성능을 개선할 수 있다.
torchvision.transforms
에는 다음과 같이 자주 쓰이는 이미지 증강 기법들이 구현되어 있다.
다음은 위에서 준비한 이미지 데이터셋에 데이터 증강을 적용하는 코드이다.
train_transform_trivial = transforms.Compose([
transforms.Resize((64, 64)),
transforms.TrivialAugmentWide(num_magnitude_bins=31), # 자동으로 다양한 이미지 증강 기법들을 적용시켜줌
transforms.ToTensor()
])
test_transform_simple = transforms.Compose([
transforms.Resize((64, 64)),
transforms.ToTensor()
])
train_data_augmented = datasets.ImageFolder(root=train_dir, transform=train_transform_trivial)
test_data_simple = datasets.ImageFolder(root=test_dir, transform=test_transform_simple)
합성곱 연산은 합성곱 신경망(CNN)의 핵심적인 요소로, 입력 데이터에서 유용한 특징을 자동으로 추출하는 역할을 한다. 이 연산은 필터(또는 커널)라고 불리는 작은 행렬을 사용하여 입력 이미지의 각 위치를 스캔하며 곱한 값을 계산한다. 그 결과로 입력 데이터의 중요한 패턴이나 특징을 강조하는 특징 맵(feature map)이 생성된다.
합성곱 연산의 주요 장점 중 하나는 파라미터 공유이다. 필터는 전체 이미지에서 동일한 가중치를 사용하므로, 학습해야 할 파라미터의 수가 크게 줄어든다. 또한 합성곱 연산은 입력 이미지의 공간적 구조를 보존하면서도 그 안의 패턴을 탐지할 수 있게 만들어 준다. 이러한 합성곱 연산 덕분에 CNN은 이미지 분류, 객체 탐지, 얼굴 인식 등 다양한 컴퓨터 비전 작업에서 매우 효과적으로 활용되고 있다.
합성곱 연산의 주요 구성요소는 다음과 같다.
필터 또는 커널(kernel)
CNN에서 필터란 작은 크기의 행렬로, 대개 3x3이나 5x5 같은 크기의 정방행렬로 만든다. 필터의 요소들(가중치)을 미리 알려진 값으로 설정하고 이미지를 처리하면 이미지에 여러 효과(컬러 이미지를 흑백으로 만들기, 윤곽선 강조하기, 흐리게 만들기 등)가 적용되기 때문에 이를 필터라고 부른다. CNN 모델에서는 이미지 처리보다는 이미지의 특징을 학습하기 위해 필터를 활용한다.
필터의 크기는 CNN 모델의 설계자가 결정하며, 필터의 요소들(가중치)은 학습 과정에서 모델이 자동으로 학습한다. 위 그림에서 곱해지는 빨간색 숫자가 바로 가중치이다.
필터의 가중치는 대개 랜덤하게 초기화되는데 주로 사용되는 초기화 방법으로는 Xavier 초기화와 Kaiming He 초기화 등이 있다. nn.Conv2d
는 기본적으로 Kaiming He 초기화 방식으로 초기화된다.
이동 간격(stride)
필터가 입력 데이터 위를 움직이는 간격을 말한다. 간격이 1이면 필터가 한 칸씩 이동하고, 2이면 두 칸씩 이동한다.
패딩 (padding)
입력 데이터의 가장자리를 처리하기 위해 0 또는 다른 값으로 채워진 픽셀을 입력 데이터의 가장자리에 추가하는 작업을 말한다. 패딩은 출력 크기를 조절하거나 가장자리 정보를 보존하기 위해 사용된다.
합성곱 층의 출력 크기 공식은 다음과 같다.
: 입력 크기, : 출력 크기, : 필터 크기, : 이동 간격, : 패딩 크기 일 때, =
출력 크기 계산 예시: 필터의 크기는 3x3, 필터의 개수는 10개, 이동간격은 2, 패딩은 0일 때
3x32x32 크기의 이미지를 입력하면 특징 맵은 가로, 세로로 의 크기를 가지므로 합성곱 층의 출력 크기는 10x15x15이 된다. 학습 대상이 되는 필터의 가중치 개수는 편향을 포함하면 개가 된다.
합성곱 신경망(CNN)은 여러 층으로 구성되어 있으며 각 층은 이미지에서 점점 더 복잡한 패턴을 학습한다. CNN의 기본 구조는 다음과 같다.
입력층은 CNN 모델에 전달되는 원시 데이터가 입력되는 층이다. 이미지 데이터가 입력되는 경우 위에서 설명했듯이 CxHxW 또는 HxWxC 형식의 텐서로 입력된다. 파이토치의 nn.Conv2d
에 이미지 텐서를 입력하려면 텐서의 형식이 (N, C, H, W)
이어야 한다. 여기서 N은 배치 크기를 의미한다.
합성곱 층은 합성곱 연산을 수행하여 입력 데이터에서 저수준의 특징(모서리, 텍스처 등)을 추출한다. 이 층에서는 여러 개의 필터가 입력 이미지에 적용되어 패턴을 인식하고 특성 맵을 생성한다. 합성곱 층은 입력 이미지의 공간적 관계를 유지하며, 여러 합성곱 층을 쌓을수록 점점 더 복잡한 특징을 학습할 수 있다. 예를 들어, 초기 층에서는 모서리나 선과 같은 단순한 패턴을 인식하지만, 층이 깊어질수록 얼굴, 객체 등과 같은 복잡한 형태를 인식할 수 있다.
합성곱 층을 만들기 위해서는 필터의 크기, 이동 간격(stride), 패딩(padding)의 크기 같은 매개변수들을 결정해야 한다. 이러한 값들에 의해 합성곱 층의 출력 크기가 계산되므로, 합성곱 층을 설계 시 항상 이러한 매개변수 값들로 계산되는 입출력 크기를 고려하며 설계해야 오류가 발생하지 않는다.
파이토치는 다양한 종류의 합성곱 층들을 제공하는데, 일반적으로 사진이나 그림 같은 2차원 이미지에 대해서 가장 많이 사용되는 것은 torch.nn.Conv2d
이며 매개변수와 입출력 크기 계산식은 다음과 같다.
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros', device=None, dtype=None)
풀링층은 합성곱 층의 출력인 특성 맵의 크기를 줄이는 역할을 하는 층으로 통합층 또는 서브샘플링층이라고도 부른다. 주로 사용되는 방식으로는 최대 풀링(Max Pooling)과 평균 풀링(Average Pooling)이 있다. 최대 풀링은 일정 영역 내에서 가장 큰 값을 선택하는 방식이며 평균 풀링은 해당 영역의 평균값을 계산하는 방식이다.
풀링층은 특성 맵의 크기를 줄여 계산량을 감소시키고, 모델이 불필요한 세부 정보에 덜 민감하도록 만든다. 이를 통해 모델의 일반화 능력이 향상되며, 과적합을 방지할 수 있다. 파이토치에선 nn.MaxPool2d
와 nn.AvgPool2d
등의 다양한 풀링층 기능을 제공한다. nn.Maxpool2d
의 매개변수와 입출력 크기 계산식은 다음과 같다.
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
풀링층 또는 합성곱층의 출력에 대해 활성함수를 적용하는 층이다. 이미지의 특징은 대개 비선형적이므로 CNN 모델에서는 대개 이러한 비선형성을 학습하기 위해 RELU 또는 RELU의 변형을 사용한다.
CNN의 합성곱층과 풀링층 등을 통과한 특징 맵을 통해 이미지를 분류하려면 위해선 특징 맵을 평탄화(flatten)해줘야 한다. 평탄화는 다차원 형태의 특징 맵을 1차원 벡터로 변환하여, 완전연결층이 이를 처리할 수 있도록 하는 작업이다. 평탄화된 특징 맵은 대개 완전연결층으로 입력된다.
CNN의 완전연결층은 딥 러닝 모델에서 흔히 쓰이는 완전연결층과 같다. 보통 CNN의 마지막 부분에서 분류를 담당하는 출력층에 완전연결층을 추가한다. 완전연결층에서 소프트맥스(softmax) 함수와 같은 활성함수를 통해 각 클래스에 대한 확률을 계산하여 최종 분류 결과를 제공한다. 이 단계에서 학습된 가중치들은 모델이 특정 입력에 대해 올바른 출력을 내도록 조정된다.
CNN에는 기본적인 합성곱, 풀링, 완전연결층 외에도 다양한 추가 요소들이 포함될 수 있다. 대표적인 예로 드롭아웃(Dropout), 배치 정규화(Batch Normalization) 등이 있다.
이 외에도 CNN 구조는 특정 문제나 데이터에 적합하도록 커스터마이징될 수 있다. 유명한 CNN 아키텍처(ResNet, VGGNet, GoogLeNet 등)들은 기본적인 CNN 구조를 확장하여 복잡한 이미지 작업을 처리할 수 있도록 설계되어있다.
다음 코드는 CNN Explainer에서 소개하는 TinyVGG 모델을 구현한 것이다.
class TinyVGG(nn.Module):
"""
Model architecture copying TinyVGG from:
https://poloclub.github.io/cnn-explainer/
"""
def __init__(self, input_shape: int, hidden_units: int, output_shape: int, padding: int = 1) -> None:
super().__init__()
self.conv_block_1 = nn.Sequential(
nn.Conv2d(
in_channels=input_shape,
out_channels=hidden_units,
kernel_size=3,
stride=1,
padding=padding
),
nn.ReLU(),
nn.Conv2d(
in_channels=hidden_units,
out_channels=hidden_units,
kernel_size=3,
stride=1,
padding=padding
),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.conv_block_2 = nn.Sequential(
nn.Conv2d(hidden_units, hidden_units, kernel_size=3, padding=padding),
nn.ReLU(),
nn.Conv2d(hidden_units, hidden_units, kernel_size=3, padding=padding),
nn.ReLU(),
nn.MaxPool2d(2)
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=hidden_units*13*13, out_features=output_shape)
)
def forward(self, x: torch.Tensor):
x = self.conv_block_1(x)
x = self.conv_block_2(x)
x = self.classifier(x)
return x
# return self.classifier(self.conv_block_2(self.conv_block_1(x))) # 이런 방식으로 코딩하면 GPU 연산이 빨라질 수 있음
device = 'cuda' if torch.cuda.is_available() else 'cpu'
torch.manual_seed(42)
model_0 = TinyVGG(
input_shape=3, # number of color channels (3 for RGB)
hidden_units=10,
output_shape=len(train_data.classes),
padding=0
).to(device)