AI & GPU
PyTorch에서 ResNet 쉽게 이해하기

PyTorch에서 ResNet을 쉽게 이해하는 방법

ResNet 소개

ResNet이란?

ResNet(Residual Neural Network)은 2015년 마이크로소프트(Microsoft)의 연구진에 의해 소개된 심층학습 아키텍처입니다. 이 아키텍처는 매우 깊은 신경망을 학습시킬 때 발생하는 사라져가거나 폭발하는 그래디언트 문제를 해결하기 위해 디자인되었습니다.

  1. Residual Neural Network: ResNet은 매우 깊은 모델의 학습을 가능하게 하는 "스킵 연결" 또는 "잔여 연결"을 활용하는 신경망 유형입니다. 이러한 스킵 연결은 네트워크가 특정 레이어를 피하도록 허용하여 사라져가는 그래디언트 문제를 완화시킵니다.

  2. 사라져가는/폭발하는 그래디언트 문제 해결: 매우 깊은 신경망에서 역전파에 사용되는 그래디언트가 전파되면서 사라져(매우 작아짐) 또는 폭발(매우 커짐)할 수 있습니다. 이는 네트워크가 효과적으로 학습하기 어렵게 만들 수 있습니다, 특히 더 깊은 레이어에서는 많이 발생합니다. ResNet의 스킵 연결은 그래디언트가 네트워크를 더 쉽게 흘러갈 수 있도록 돕는 역할을 합니다.

ResNet의 장점

  1. 깊은 신경망에서 성능 향상: ResNet의 스킵 연결은 깊은 신경망의 학습을 가능하게 하여, 이미지 분류, 객체 검출 및 의미적 분할과 같은 다양한 작업에서 큰 성능 향상을 이끌어냅니다.

  2. 훈련 중 빠른 수렴: ResNet의 스킵 연결은 네트워크를 통해 그래디언트가 더 효율적으로 흘러갈 수 있도록 함으로써, 훈련 과정에서 네트워크가 더 빨리 수렴하도록 돕습니다.

PyTorch에서 ResNet 구현하기

환경 설정

  1. PyTorch 설치: PyTorch로 ResNet을 구현하기 위해서는 먼저 PyTorch 라이브러리를 설치해야 합니다. 공식 웹사이트(https://pytorch.org/)에서 (opens in a new tab) 운영체제와 파이썬 버전에 맞는 PyTorch를 다운로드하고 설치할 수 있습니다.

  2. 필요한 라이브러리 가져오기: PyTorch를 설치한 후, 프로젝트에 필요한 라이브러리를 가져와야 합니다. 일반적으로 이에는 PyTorch, NumPy 및 데이터 전처리, 시각화 또는 기타 작업에 필요한 다른 라이브러리가 포함됩니다.

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt

ResNet 아키텍처 정의

기본적인 구성 요소 이해하기

  1. 합성곱층(Convolutional Layers): ResNet은 다른 많은 딥러닝 모델과 마찬가지로 기본적인 특징 추출을 위해 합성곱층을 사용합니다.

  2. 배치 정규화(Batch Normalization): ResNet은 훈련 과정을 안정화시키고 모델의 성능을 향상하기 위해 배치 정규화층을 사용합니다.

  3. 활성화 함수(Activation Functions): ResNet 아키텍처는 일반적으로 렐루(Rectified Linear Unit)를 활성화 함수로 사용합니다. 이를 통해 모델에 비선형성을 도입할 수 있습니다.

  4. 풀링 층(Pooling Layers): ResNet은 최대 풀링 또는 평균 풀링과 같은 풀링 층을 포함하여 특징 맵의 공간 차원을 줄이고 변위 불변성을 도입할 수 있습니다.

ResNet 블록 구현하기

  1. 잔여 연결(Residual Connection): ResNet의 주요 개념은 입력을 출력에 더하는 것으로 특정 레이어를 통과시키는 것에 대한 스킵 연결입니다. 이를 통해 사라져가는 그래디언트 문제를 완화할 수 있습니다.
class ResNetBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super(ResNetBlock, 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.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )
 
    def forward(self, x):
        residual = self.shortcut(x)
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        out += residual
        out = self.relu(out)
        return out
  1. 스킵 연결(Shortcut Connection): 잔여 연결 외에도 ResNet은 필요에 따라 입력과 출력의 차원을 맞추기 위해 "스킵 연결"을 활용합니다.

전체 ResNet 모델 구성하기

  1. ResNet 블록 쌓기: 전체 ResNet 모델을 생성하기 위해서는 여러 개의 ResNet 블록을 쌓아야 합니다. 각 블록의 레이어 수와 필터 수를 조정해야 합니다.

  2. 레이어 수 조정하기: ResNet 모델은 ResNet-18, ResNet-34, ResNet-50, ResNet-101 및 ResNet-152 등 다양한 변형으로 제공되며, 각각 다른 레이어 수를 가지고 있습니다. 레이어 수는 모델의 복잡성과 성능에 영향을 미칩니다.

PyTorch에서 ResNet-18 구현하기

ResNet-18 모델 정의하기

  1. 입력 층: ResNet-18 모델의 입력 층은 일반적으로 224x224 픽셀과 같은 특정 크기의 이미지를 입력으로 받습니다.

  2. 합성곱층(Convolutional Layers): ResNet-18 모델의 초기 합성곱층은 입력 이미지에서 기본적인 특징을 추출합니다.

  3. ResNet 블록: ResNet-18 모델의 핵심은 여러 개의 ResNet 블록을 쌓아 더 깊은 네트워크를 훈련하는 것입니다.

  4. 완전 연결층(Fully Connected Layer): 합성곱 및 ResNet 블록 후에 모델에는 최종 분류 또는 예측 작업을 수행하는 완전 연결층이 있습니다.

  5. 출력 층: ResNet-18 모델의 출력 층은 해결하려는 문제의 클래스 수와 동일한 수의 유닛을 갖게 됩니다.

class ResNet18(nn.Module):
    def __init__(self, num_classes=10):
        super(ResNet18, 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(64, 64, 2, stride=1)
        self.layer2 = self._make_layer(64, 128, 2, stride=2)
        self.layer3 = self._make_layer(128, 256, 2, stride=2)
        self.layer4 = self._make_layer(256, 512, 2, stride=2)
 
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512, num_classes)
 
    def _make_layer(self, in_channels, out_channels, num_blocks, stride):
        layers = []
        layers.append(ResNetBlock(in_channels, out_channels, stride))
        self.in_channels = out_channels
        for i in range(1, num_blocks):
            layers.append(ResNetBlock(out_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 = x.view(x.size(0), -1)
        x = self.fc(x)
        return x

모델 초기화하기

ResNet-18 모델의 인스턴스를 생성하려면 단순히 ResNet18 클래스를 인스턴스화하면 됩니다:

model = ResNet18(num_classes=10)

모델 요약 출력하기

torchsummary 라이브러리의 summary() 함수를 사용하여 ResNet-18 모델 구조에 대한 요약을 출력할 수 있습니다:

from torchsummary import summary
summary(model, input_size=(3, 224, 224))

이는 모델의 레이어와 각 레이어의 파라미터 수 및 출력 형태에 대한 자세한 개요를 제공합니다.

ResNet-18 모델 훈련하기

데이터셋 준비하기

데이터셋 다운로드 및 로드하기

이 예제에서는 이미지 분류 작업의 대표적인 벤치마크인 CIFAR-10 데이터셋을 사용합니다. torchvision.datasets.CIFAR10 모듈을 사용하여 데이터셋을 다운로드할 수 있습니다:

# CIFAR-10 데이터셋 다운로드 및 로드하기
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transforms.ToTensor())
test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transforms.ToTensor())

데이터 전처리하기

모델을 훈련하기 전에 데이터를 전처리해야 합니다. 예를 들어 픽셀 값을 정규화하고 데이터 증가 기법을 적용할 수 있습니다:

# 데이터 전처리 정의하기
transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
 
transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
 
# 데이터 로더 생성하기
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=2)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=100, shuffle=False, num_workers=2)

훈련 루프 정의하기

장치 설정하기(CPU 또는 GPU)

GPU 가속을 활용하기 위해 모델과 데이터를 GPU로 이동시킬 수 있습니다:

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)

손실 함수와 옵티마이저 정의하기

다음으로, 훈련 과정 중에 사용할 손실 함수와 옵티마이저를 정의해야 합니다:

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)

훈련 루프 구현하기

훈련 루프는 다음과 같은 단계로 이루어집니다:

  1. 모델을 통한 순전파 (Forward pass)
  2. 손실 계산하기
  3. 그라디언트 (gradient) 역전파하기
  4. 모델 파라미터 업데이트하기
  5. 훈련 손실과 정확도 추적하기
num_epochs = 100
train_losses = []
train_accuracies = []
val_losses = []
val_accuracies = []
 
for epoch in range(num_epochs):
    # 훈련 단계
    model.train()
    running_loss = 0.0
    correct = 0
    total = 0
    for i, (inputs, labels) in enumerate(train_loader):
        inputs, labels = inputs.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
 
## 모델 최적화
 
### 정규화
 
정규화는 딥러닝 모델에서 오버피팅을 방지하기 위해 사용되는 기술입니다. 오버피팅은 모델이 훈련 데이터에서는 잘 작동하지만 새로운, 이전에 보지 못한 데이터에는 일반화할 수 없는 경우 발생합니다. 정규화 기법은 모델의 복잡성에 페널티를 부여하거나 훈련 과정에 노이즈를 추가함으로써 모델이 더 잘 일반화되도록 도와줍니다.
 
L2 정규화라고도 알려진 가장 인기 있는 정규화 기법 중 하나는 가중치의 제곱 크기에 비례하는 패널티 항을 손실 함수에 추가하는 것입니다. L2 정규화가 적용된 손실 함수는 다음과 같이 작성할 수 있습니다:
 

loss = original_loss + lambda * sum(w^2)


여기서 `lambda`는 정규화 강도를 나타내며, `w`는 모델의 가중치입니다.

또 다른 인기 있는 정규화 기법은 Dropout입니다. Dropout은 훈련 중에 레이어의 일부 활성화를 무작위로 0으로 설정하여 모델의 용량을 줄이고 보다 견고한 특성을 배우도록 만듭니다. 이를 통해 오버피팅을 방지하고 모델의 일반화 성능을 향상시킬 수 있습니다.

다음은 PyTorch 모델에 Dropout을 구현하는 예시입니다:

```python
import torch.nn as nn

class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.fc1 = nn.Linear(64, 128)
        self.dropout = nn.Dropout(p=0.5)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.fc1(x)
        x = self.dropout(x)
        x = self.fc2(x)
        return x

이 예시에서는 Dropout 레이어를 첫 번째 완전 연결 레이어 다음에 적용하고, Dropout 비율을 0.5로 설정하여 50%의 활성화를 훈련 중에 무작위로 0으로 설정합니다.

옵티마이저 알고리즘

옵티마이저 알고리즘의 선택은 딥러닝 모델의 성능과 수렴에 큰 영향을 줄 수 있습니다. 딥러닝에서 사용되는 몇 가지 인기 있는 옵티마이저 알고리즘은 다음과 같습니다:

확률적 경사 하강법 (SGD)

SGD는 가장 기본적인 옵티마이저 알고리즘으로, 그라디언트를 하나의 훈련 예시 또는 작은 배치에서 계산하고 이에 따라 가중치를 업데이트합니다. SGD는 수렴이 느릴 수 있지만 간단하고 효과적입니다.

import torch.optim as optim
 
model = MyModel()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

Adam

Adam (Adaptive Moment Estimation)은 각 파라미터에 대해 적응적 학습률을 계산하는 좀 더 고급화된 옵티마이저 알고리즘입니다. 이는 운동량 (momentum)과 RMSProp의 장점을 결합하여 많은 딥러닝 작업에서 인기 있는 선택지입니다.

optimizer = optim.Adam(model.parameters(), lr=0.001)

AdaGrad

AdaGrad (Adaptive Gradient)는 과거 그라디언트를 기반으로 각 파라미터에 대한 학습률을 적응적으로 조정하는 옵티마이저 알고리즘입니다. 희소 데이터에 효과적이지만 시간이 지남에 따라 학습률이 지나치게 감소할 수 있습니다.

optimizer = optim.Adagrad(model.parameters(), lr=0.01)

RMSProp

RMSProp (Root Mean Square Propagation)은 이동 평균을 이용하여 제곱 그라디언트의 평균을 유지하는 다른 적응적 학습률 옵티마이저 알고리즘입니다. 반복되는 신경망과 같은 비정상적인 목적에 특히 효과적입니다.

optimizer = optim.RMSprop(model.parameters(), lr=0.001, alpha=0.99)

옵티마이저 알고리즘의 선택은 특정 문제, 모델 구조 및 데이터의 특성에 따라 달라집니다. 자신의 작업에 대해 다른 알고리즘을 실험하고 성능을 비교하는 것이 좋은 아이디어입니다.

전이학습

전이학습은 큰 데이터셋에서 훈련된 모델을 동일하지 않지만 관련된 작업에 대한 모델의 출발점으로 사용하는 기술입니다. 이는 대상 데이터셋이 작을 때 특히 유용한데, 이를 통해 모델은 큰 데이터셋에서 학습한 특징을 활용할 수 있습니다.

딥러닝에서의 전이학습 방법 중 하나는, 컴퓨터 비전이나 자연어 처리 작업에 사용 가능한 사전 훈련된 모델을 사용하고 대상 데이터셋에서 이를 세밀하게 조정하는 것입니다. 이를 위해 사전 훈련된 모델의 하위 레이어를 동결하고 새로운 데이터로만 상위 레이어를 훈련합니다.

다음은 PyTorch에서 이미지 분류 작업을 위해 사전 훈련된 ResNet 모델을 세밀하게 조정하는 예시입니다:

import torchvision.models as models
import torch.nn as nn
 
# 사전 훈련된 ResNet 모델 불러오기
resnet = models.resnet18(pretrained=True)
 
# 사전 훈련된 모델의 파라미터 동결하기
for param in resnet.parameters():
    param.requires_grad = False
 
# 마지막 레이어를 새로운 완전 연결 레이어로 대체하기
num_features = resnet.fc.in_features
resnet.fc = nn.Linear(num_features, 10)  # 10개 클래스 가정
 
# 새로운 데이터셋으로 모델 훈련하기
optimizer = optim.Adam(resnet.fc.parameters(), lr=0.001)

이 예시에서는 먼저 사전 훈련된 ResNet18 모델을 불러오고, 하위 레이어의 파라미터를 동결합니다. 그런 다음 완전 연결 레이어를 대상 작업에 맞는 새로운 레이어로 대체합니다 (이 예시에서는 10개의 클래스를 가정합니다). 마지막으로 Adam 옵티마이저를 사용하여 새로운 완전 연결 레이어의 파라미터만 업데이트하여 모델을 훈련합니다.

전이학습은 특히 대상 데이터셋이 작을 때 딥러닝 모델의 성능을 크게 향상시킬 수 있습니다. 이는 모델 개발 과정에서 시간과 리소스를 절약할 수 있는 강력한 기술입니다.

모델 해석 가능성

딥러닝 모델이 더 복잡하고 널리 사용되면서, 모델의 해석 가능성이 점점 더 중요해지고 있습니다. 해석 가능성은 모델의 내부 결정 과정을 이해하고 설명할 수 있는 능력을 의미합니다.

모델 해석 가능성을 향상시키기 위한 인기 있는 기술 중 하나는 어텐션 메커니즘의 사용입니다. 어텐션은 모델이 예측을 수행할 때 가장 관련 있는 입력 부분에 집중할 수 있게 해주며, 어텐션 가중치는 어떤 특성을 사용하는지 이해하기 위해 시각화할 수 있습니다.

다음은 자연어 처리 작업을 위한 PyTorch 모델에 어텐션 메커니즘을 구현하는 예시입니다:

import torch.nn as nn
import torch.nn.functional as F
 
class AttentionModel(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        super(AttentionModel, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, bidirectional=True, batch_first=True)
        self.attention = nn.Linear(hidden_dim * 2, 1)
 
    def forward(self, input_ids):
        # 입력 임베딩
        embedded = self.embedding(input_ids)
 
        # 임베딩된 입력을 LSTM에 통과
        lstm_output, _ = self.lstm(embedded)
 
        # 어텐션 가중치 계산
        attention_weights = F.softmax(self.attention(lstm_output), dim=1)
 
        # LSTM 출력에 가중치의 합 계산
        context = torch.sum(attention_weights * lstm_output, dim=1)
 
        return context

이 예시에서 어텐션 메커니즘은 LSTM 출력을 입력으로 받고 어텐션 가중치를 출력하는 선형 레이어로 구현되어 있습니다. 이 가중치는 LSTM 출력의 가중합을 계산하는 데 사용되며, 모델의 최종 출력입니다.

어텐션 가중치를 시각화함으로써 모델이 예측을 수행할 때 어떤 입력 부분에 집중하는지 파악할 수 있습니다. 이를 통해 모델의 결정 과정을 이해하고 잠재적인 편향이나 개선할 부분을 식별할 수 있습니다.

모델 해석 가능성을 향상시키기 위한 또 다른 기술로는 특성 중요도 분석이 있습니다. 이는 모델이 예측을 수행하는 데 사용하는 가장 중요한 특성을 식별하는 것을 포함합니다. 이를 위해 가장 인기 있는 방법 중 하나는 Shapley 값입니다. Shapley 값은 각 특성이 모델의 출력에 기여하는 정도를 측정하는 방법을 제공합니다.

모델의 해석 가능성 향상은 딥러닝에서 중요한 연구 분야로, 이를 통해 이러한 강력한 모델을 신뢰할 수 있게 하고 책임있게 사용할 수 있습니다.

결론

이 자습서에서는 딥러닝과 관련된 여러 주제, 모델 최적화, 전이학습, 모델 해석 가능성 등을 다루었습니다. 정규화, 옵티마이저 알고리즘, 어텐션 메커니즘과 같은 기법에 대해 이야기하고, 이러한 개념을 PyTorch에서 어떻게 구현하는지에 대한 예시를 제공했습니다.

딥러닝은 계속해서 발전하고 널리 채택되는 분야이며, 이러한 고급 주제를 이해하고 자신의 프로젝트에 적용하는 것이 중요합니다. 이러한 기술을 숙달함으로써 고성능, 견고성이 뛰어나며 해석 가능한 딥러닝 모델을 구축하는 데 더 잘 준비될 수 있습니다.

기억해야 할 점은, 딥러닝은 빠르게 변화하는 분야이며, 최신 연구와 최상의 방법론을 따라가는 것이 중요합니다. 계속해서 탐색, 실험, 학습을 진행하면서 딥러닝 전문가로 발전할 수 있습니다.