ResNet(Residual Network)
깊은 신경망을 효과적으로 학습하기 위해 개발된 모델
잔차 학습(Residual Learning) 개념을 도입하여 기울기 소실(Vanishing Gradient)문제를 해결
2015년 Microsoft Research에서 개발었으며, ImageNet 챌린지(ILSVRC) 2015에서 우승
신경망의 깊이가 깊어질수록 더 복잡한 패턴을 학습 할 수 있지만, 오차 역전파 시 기울기가 매우 작아지거나 커져 가중치 업데이트가 제대로 이루어지지 않는 기울기 소실(Vanishing Gradient) 또는 기울기 폭발(Exploding Gradient) 문제로 인해 학습이 어려워 짐
→ 네트워크를 깊게 쌓을수록 성능이 오히려 저하되는 문제 발생
잔차 학습(Residual Learning)
ResNet은 잔차(Residual)를 학습하는 구조를 사용
일반적인 신경망은 각 레이어의 출력을 바로 다음 레이어로 전달하지만, ResNet에서는 입력을 출력에 더하는(Shorcut Connection) 방식을 사용
→ 기울기가 원활하게 전달되면서 깊은 네트워크에서도 안정적인 학습이 가능
잔차 블록(Residual Block)의 수식 표현
일반적인 네트워크의 출력
Y = F(X)
ResNet의 출력 (잔차 학습 적용)
Y = F(X) + X
여기서 F(X)는 컨볼루션 연산을 의미하며, X를 직접 더해주는 것이 핵심
ResNet의 주요 특징
기울기 소실 문제 해결
Shortcut Connection을 통해 원래 입력을 출력과 더해주어 신호가 손실되지 않고 전달
→ 깊은 네트워크에서도 학습이 원활하게 이루어짐
Residual Block을 통한 네트워크 확장
Residual Block을 사용하여 쉽게 네트워크를 깊게 확장 가능
ResNet-18, ResNet-34, ResNet-50, ResNet-101, ResNet-152 등 다양한 버전 존재
*ResNet-50 / ResNet-101 / ResNet-152는 ImageNet 데이터셋에서 높은 정확도를 기록함
→ 더 깊은 모델이더라도 성능이 하락하지 않고 안정적으로 유지됨
높은 성능과 다양한 응용 분야
이미지 분류(Image Classification), 객체 탐지(Objet Detection), 시맨틱 세그멘테이션(Semantic Segmentation), GAN(Generative Adversarial Network, 생성적 적대 신경망) 등 컴퓨터 비전 분야에서 우수한 성능을 발휘
ResNet 모델 구조
Residual Block (기본 블록)
ResNet의 핵심 단위
입력을 직접 출력과 더하는 Shorcut Connection을 포함하고 있음
- Conv → BatchNorm → ReLU
- Conv → BatchNorm
- 입력값과 합산 후 활성화 함수 적용
ResNet 전체 구조
- 초기 계층 : ConV + BatchNorm + ReLU
- Residual Blocks : 여러 개의 Residual Block이 포함됨 (ResNet-18 기준: [2, 2, 2, 2])
- Global Average Pooling
- Fully Connected Layer (출력층)
ResNet 구현 (PyTorch)
PyTorch를 사용한 ResNet-18 구현
import torch
import torch.nn as nn
import torch.nn.functional as F
# Residual Block 정의
class Block(nn.Module):
def __init__(self, in_ch, out_ch, stride=1):
"""
Residual Block (잔차 블록)
- 두 개의 3x3 컨볼루션 레이어(conv1, conv2)와 Batch Normalization을 포함
- 입력이 shortcut을 통해 출력에 더해지는 구조
Args:
- in_ch (int): 입력 채널 수
- out_ch (int): 출력 채널 수
- stride (int, optional): 첫 번째 컨볼루션의 stride (기본값: 1)
"""
super(Block, self).__init__()
# 첫 번째 3x3 컨볼루션 레이어 (stride 적용 가능)
self.conv1 = nn.Conv2d(in_ch, out_ch, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_ch) # 배치 정규화
# 두 번째 3x3 컨볼루션 레이어 (채널 수 유지, stride=1)
self.conv2 = nn.Conv2d(out_ch, out_ch, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_ch) # 배치 정규화
# 입력과 출력 채녈이 다를 경우 1x1 Conv를 사용하여 차원 맞추기
self.skip_connection = nn.Sequential()
if stride != 1 or in_ch != out_ch:
self.skip_connection = nn.Sequential(
nn.Conv2d(in_ch, out_ch, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_ch) # 배치 정규화
)
def forward(self, x):
"""
Forward 함수 (잔차 학습 적용)
- 입력 데이터 (x)를 컨볼루션과 배치 정규화를 거쳐 변환
- Shortcut Connection을 통해 원래 입력(x)을 출력에 더함
"""
# 첫 번째 컨볼루션 + ReLU 활성화 함수
output = F.relu(self.bn1(self.conv1(x)))
# 두 번째 컨볼루션 후 배치 정규화
output = self.bn2(self.conv2(output))
# shortcut 경로 출력과 현재 블록의 출력 더하기
output += self.skip_connection(x) # Residual 연결
# 최종 ReLU 활성화 함수 적용
output = F.relu(output)
return output
# ResNet 모델 정의
class CustomResNet(nn.Module):
def __init__(self, block, layers, num_classes=10):
"""
Custom ResNet 모델
- 첫 번째 컨볼루션 레이어와 배치 정규화 적용
- Residual Block을 쌓아 네트워크 구성
- 최종적으로 Fully Connected Layer를 통해 분류 수행
Args:
- block (nn.Module): Residual Block 클래스
- layers (list): 각 단계에서 사용할 Residual Block의 개수
- num_classes (int, optional): 최종 분류할 클래스 개수 (기본값: 10)
"""
super(CustomResNet, self).__init__()
self.initial_channels = 64 # 첫 번째 레이어의 입력 채널 수 정의
# 첫 번째 컨볼루션 레이어 (입력: 3채널 이미지)
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(64) # 배치 정규화
# ResNet의 각 레이어 생성 (Residual Block을 여러 개 쌓음)
self.layer1 = self._create_layer(block, 64, layers[0], stride=1)
self.layer2 = self._create_layer(block, 128, layers[1], stride=2)
self.layer3 = self._create_layer(block, 256, layers[2], stride=2)
self.layer4 = self._create_layer(block, 512, layers[3], stride=2)
# 평균 풀링 레이어 (AdaptiveAvgPool2d: 입력 크기에 관계없이 1x1로 변환)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
# 최종 완전 연결 레이어 (출력: num_classes)
self.fc = nn.Linear(512, num_classes)
# ResNet의 각 레이어를 생성하는 함수
def _create_layer(self, block, out_ch, num_layers, stride):
"""
Residual Block을 여러 개 쌓아 하나의 레이어를 구성하는 함수
Args:
- block (nn.Module): Residual Block 클래스
- out_ch (int): 출력 채널 수
- num_layers (int): 해당 레이어에서 사용할 Residual Block 개수
- stride (int): 첫 번째 블록에서 적용할 stride 값
Returns:
- nn.Sequential: 구성된 Residual Layer
"""
layers= []
# 첫 번째 블록은 stride를 받을 수 있음
layers.append(block(self.initial_channels, out_ch, stride))
self.initial_channels = out_ch # 다음 불록을 위해 채널 수 업데이트
# 나머지 블록들은 기본 stride를 사용
for _ in range(1, num_layers):
layers.append(block(out_ch, out_ch))
return nn.Sequential(*layers)
def forward(self, x):
"""
Forward 함수 (ResNet 모델의 순전파 과정)
- 첫 번째 컨볼루션을 거친 후, 여러 개의 Residual Block을 순차적으로 통과
- 마지막으로 Fully Connected Layer를 거쳐 최종 클래스 확률을 출력
"""
# 첫 번째 컨볼루션 + ReLU 활성화 함수
x = F.relu(self.bn1(self.conv1(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) # 1차원 벡터로 변환
# 최종 완전 연결 레이어를 통해 클래스별 예측값 출력
x = self.fc(x)
return x
# ResNet-18 모델 생성 (각 레이어의 블록 수 : [2, 2, 2, 2])
model = CustomResNet(Block, [2, 2, 2, 2], num_classes=10)
import torch
import torchvision
import torchvision.transforms as transforms
# 데이터 변환 (Normalization 포함)
transform = transforms.Compose([
transforms.ToTensor(), # 이미지를 Tensor로 변환
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 정규화
])
# CIFAR-10 데이터셋 다운로드 및 로드
batch_size = 128
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=False, num_workers=2)
# CIFAR-10 클래스 목록
classes = ('airplane', 'automobile', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck')
import torch.optim as optim
# GPU 사용 여부 확인
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
# 모델 생성 및 GPU로 이동
model = CustomResNet(Block, [2, 2, 2, 2], num_classes=10).to(device)
# 손실 함수 (CrossEntropy Loss) 및 최적화 함수 (Adam 사용)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 학습 루프
num_epochs = 10 # 학습 횟수
for epoch in range(num_epochs):
running_loss = 0.0
correct = 0
total = 0
for i, (inputs, labels) in enumerate(trainloader):
inputs, labels = inputs.to(device), labels.to(device) # GPU 이동
optimizer.zero_grad() # 기존 기울기 초기화
outputs = model(inputs) # 모델 예측
loss = criterion(outputs, labels) # 손실 계산
loss.backward() # 역전파 (Backpropagation)
optimizer.step() # 가중치 업데이트
# 손실 및 정확도 기록
running_loss += loss.item()
_, predicted = torch.max(outputs, 1)
correct += (predicted == labels).sum().item()
total += labels.size(0)
if i % 100 == 99: # 100배치마다 진행 상황 출력
print(f"[Epoch {epoch+1}, Batch {i+1}] Loss: {running_loss / 100:.4f}, Accuracy: {100 * correct / total:.2f}%")
running_loss = 0.0
"""
Using device: cpu
[Epoch 1, Batch 100] Loss: 1.7272, Accuracy: 36.34%
[Epoch 1, Batch 200] Loss: 1.3554, Accuracy: 43.12%
[Epoch 1, Batch 300] Loss: 1.1655, Accuracy: 47.93%
[Epoch 2, Batch 100] Loss: 0.8738, Accuracy: 68.91%
[Epoch 2, Batch 200] Loss: 0.8252, Accuracy: 69.99%
[Epoch 2, Batch 300] Loss: 0.7599, Accuracy: 71.09%
[Epoch 3, Batch 100] Loss: 0.6132, Accuracy: 79.12%
[Epoch 3, Batch 200] Loss: 0.5797, Accuracy: 79.45%
[Epoch 3, Batch 300] Loss: 0.5677, Accuracy: 79.65%
[Epoch 4, Batch 100] Loss: 0.4474, Accuracy: 84.36%
[Epoch 4, Batch 200] Loss: 0.4410, Accuracy: 84.59%
[Epoch 4, Batch 300] Loss: 0.4530, Accuracy: 84.48%
[Epoch 5, Batch 100] Loss: 0.3366, Accuracy: 88.33%
[Epoch 5, Batch 200] Loss: 0.3562, Accuracy: 88.13%
[Epoch 5, Batch 300] Loss: 0.3547, Accuracy: 87.97%
[Epoch 6, Batch 100] Loss: 0.2294, Accuracy: 92.12%
[Epoch 6, Batch 200] Loss: 0.2708, Accuracy: 91.31%
[Epoch 6, Batch 300] Loss: 0.2757, Accuracy: 90.92%
[Epoch 7, Batch 100] Loss: 0.1596, Accuracy: 94.44%
[Epoch 7, Batch 200] Loss: 0.1982, Accuracy: 93.71%
[Epoch 7, Batch 300] Loss: 0.2070, Accuracy: 93.44%
[Epoch 8, Batch 100] Loss: 0.1244, Accuracy: 95.66%
[Epoch 8, Batch 200] Loss: 0.1199, Accuracy: 95.75%
[Epoch 8, Batch 300] Loss: 0.1443, Accuracy: 95.51%
[Epoch 9, Batch 100] Loss: 0.0861, Accuracy: 97.11%
[Epoch 9, Batch 200] Loss: 0.0983, Accuracy: 96.79%
[Epoch 9, Batch 300] Loss: 0.1032, Accuracy: 96.65%
[Epoch 10, Batch 100] Loss: 0.0829, Accuracy: 96.96%
[Epoch 10, Batch 200] Loss: 0.0678, Accuracy: 97.30%
[Epoch 10, Batch 300] Loss: 0.0866, Accuracy: 97.20%
"""
# 모델 평가 모드 설정
model.eval()
correct = 0
total = 0
with torch.no_grad(): # 테스트 과정에서는 기울기 계산 X (속도 향상)
for images, labels in testloader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs, 1) # 가장 높은 확률의 클래스 선택
correct += (predicted == labels).sum().item()
total += labels.size(0)
# 최종 정확도 출력
print(f"Test Accuracy: {100 * correct / total:.2f}%")
"""
Test Accuracy: 83.19%
"""
CPU로 돌렸더니 약 7-8시간 걸렸다.. MPS로 재시도 하는 중
CNN 성능은 약 75%정도 나왔던 것 같은데.. 나름 만족
'⊢ DeepLearning' 카테고리의 다른 글
오토인코더(Autoencoder) (0) | 2025.03.21 |
---|---|
이미지 처리 모델 (0) | 2025.03.21 |
자연어 처리(Natural Language Processing, NLP) 모델 (0) | 2025.03.20 |
어텐션(Attention) 메커니즘 (1) | 2025.03.20 |
순환 신경망(Recrurrent Neural Network, RNN) (0) | 2025.03.20 |