딥러닝 파이토치 강좌, ResNet

딥러닝 분야에서 Residual Network, 줄여서 ResNet은 매우 중요한 아키텍처로 자리 잡았습니다. ResNet은 2015년 Kaiming He에 의해 제안되었으며, 특히 딥러닝 모델의 깊이를 효과적으로 증가시킬 수 있는 방법을 제공합니다. 현대의 다양한 컴퓨터 비전 문제들에서 ResNet은 성능 향상의 주요 원인 중 하나로 꼽힙니다.

1. ResNet의 개요

ResNet은 “Residual Learning” 프레임워크를 기반으로 한 신경망입니다. 전통적으로, 심층 신경망(dnn)은 더 깊어질수록 성능의 저하가 발생하는 경향이 있습니다. 이는 주로 기울기 소실(vanishing gradient) 문제 때문인데, 이 문제는 신경망의 깊이가 깊어질수록 역전파 과정에서 기울기가 소실되어 가는 현상입니다.

ResNet은 이러한 문제를 해결하기 위해 잔차 연결(residual connection)을 도입하였습니다. 잔차 연결은 네트워크의 입력을 출력에 더함으로써 한 계층에서 이전 계층의 정보를 직접 전달합니다. 이러한 방식을 통해 더 깊은 네트워크를 효과적으로 학습할 수 있습니다.

2. ResNet의 구조

ResNet은 다양한 깊이를 가진 모델로 구성될 수 있으며, 일반적으로 “ResNet50”, “ResNet101”, “ResNet152″와 같은 식으로 표기됩니다. 이 숫자는 네트워크의 총 층 수를 의미합니다.

2.1 기본 블록 구성

ResNet의 기본 구성 요소는 다음과 같은 블록으로 이루어져 있습니다:

  • 컨볼루션 레이어
  • Batch Normalization
  • ReLU 활성화 함수
  • 잔차 연결

일반적인 ResNet 블록의 구조는 다음과 같습니다:


def resnet_block(input_tensor, filters, kernel_size=3, stride=1):
    x = Conv2D(filters, kernel_size=kernel_size, strides=stride, padding='same')(input_tensor)
    x = BatchNormalization()(x)
    x = ReLU()(x)
    x = Conv2D(filters, kernel_size=kernel_size, strides=stride, padding='same')(x)
    x = BatchNormalization()(x)
    
    shortcut = Conv2D(filters, kernel_size=1, strides=stride, padding='same')(input_tensor)
    x = Add()([x, shortcut])
    x = ReLU()(x)
    
    return x

3. PyTorch를 이용한 ResNet 구현

이제 파이토치(Pytorch)를 사용하여 ResNet을 구현해 보겠습니다. 먼저 필요한 라이브러리를 설치합니다:

pip install torch torchvision

이후, 다음으로 기본 ResNet 모델을 구현합니다:


import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torchvision.models as models

class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = downsample

    def forward(self, x):
        identity = x
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity
        out = self.relu(out)

        return out

class ResNet(nn.Module):
    def __init__(self, block, layers, num_classes=1000):
        super(ResNet, self).__init__()
        self.in_channels = 64
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * block.expansion, num_classes)

    def _make_layer(self, block, out_channels, blocks, stride=1):
        downsample = None
        if stride != 1 or self.in_channels != out_channels * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channels, out_channels * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels * block.expansion),
            )
        layers = []
        layers.append(block(self.in_channels, out_channels, stride, downsample))
        self.in_channels = out_channels * block.expansion
        for _ in range(1, blocks):
            layers.append(block(self.in_channels, out_channels))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x

def resnet18(num_classes=1000):
    return ResNet(BasicBlock, [2, 2, 2, 2], num_classes)

3.1 모델 훈련 준비하기

ResNet 모델을 훈련하기 위해 데이터셋을 준비하고, 옵티마이저와 손실 함수를 설정합니다.


# 데이터셋 준비
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
])

train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True)

# 모델 초기화
model = resnet18(num_classes=10)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

3.2 훈련 단계

이제 모델을 훈련시킬 준비가 되었습니다:


for epoch in range(10): # epochs 설정
    model.train()  # 모델을 훈련 모드로 전환
    for images, labels in train_loader:
        optimizer.zero_grad()  # 기울기 초기화
        outputs = model(images)  # 모델 예측
        loss = criterion(outputs, labels)  # 손실 계산
        loss.backward()  # 역전파
        optimizer.step()  # 파라미터 업데이트

    print(f'Epoch [{epoch+1}/10], Loss: {loss.item():.4f}')

4. ResNet의 활용

ResNet은 다양한 컴퓨터 비전 태스크에서 사용 가능합니다. 예를 들어, 이미지 분류, 객체 탐지, 세분화, 그리고 더 복잡한 비전 문제에 이르기까지 폭넓게 활용됩니다. Google, Facebook 등이 사용하는 여러 이미지 및 비디오 태스크에 ResNet 아키텍처가 포함되어 있습니다.

5. 결론

이번 강좌에서는 ResNet의 기본 개념 및 아키텍처에 대해 알아보았고, 파이토치를 이용하여 기본 ResNet 모델을 구현하는 방법을 배워보았습니다. 딥러닝 모델을 더 깊게 쌓을 수 있는 유연한 방법과 잔차 학습을 활용하여 더 나은 성능을 낼 수 있는 기회를 제공하는 ResNet은 많은 연구자와 개발자에게 영감을 주는 주요 아키텍처입니다.

이제 더 심화된 ResNet 구조 및 다양한 파라미터 조정, 데이터 증강 기법 등을 통한 모델 개선을 공부해 볼 수 있습니다.

6. 참고 자료