이 블로그는 국민대학교 김영욱 교수님의 "인공지능" 교과목을 듣고 과제를 리뷰로 작성한 블로그입니다.
오늘은 인공지능 수업의 두 번째 과제인 Multi-layer perceptron, Convolution Neural Netwrok 구현을 해보았다.
그 중에서도 CNN 구현을 한것에 대해 리뷰를 해보겠다. ResNet을 사용하여 성능을 끌어올렸다
코드와 아키텍처 구조, 작동 매커니즘에 대해 알아보자.
다음 모델은 CIFAR-100 데이터셋의 복잡한 이미지 분류 작업을 효과적으로 처리하기 위해 설계되었으며, 깊은 네트워크 구조를 통해 우수한 성능을 보였다.
# PyTorch 라이브러리 import *** 해당 cell을 수정하지 말 것 ***
import torch
import torch.nn as nn
from torchvision import transforms, datasets
torch와 신경망, torchvision을 import 시켜준다.
# TODO: CIFAR-100 training set 불러오기
transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomCrop(32, padding=4),
transforms.ToTensor(),
])
train_dataset = datasets.CIFAR100(root='.', train=True, download=True,
transform=transform)
batch_size = 512
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size, shuffle=True)
위의 코드는 CIFAR-100 데이터셋을 불러와서 Data preprocessing 및 data loader를 설정하는 부분이다 또한 데이터 증강, RandomHorizontalFlip과 RandomCrop을 사용하여 데이터 증강을 수행함으로써 모델의 일반화 성능을 향상시키고, 과적합(overfitting)을 방지하였다.
• shuffle=True를 설정하여 매 에포크마다 데이터 순서를 섞어줌으로써 모델이 데이터 순서에 의존하지 않도록 한다.
• 배치 크기를 512로 설정하여 한 번에 많은 데이터를 처리함으로써 GPU 활용도를 높일 수 있다.
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
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.GELU()
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.stride = stride
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))
else:
self.shortcut = nn.Identity()
def forward(self, x):
identity = self.shortcut(x)
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out += identity
out = self.relu(out)
return out
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
torch.manual_seed(2024) # 결과 재현을 위한 seed number 고정 *** 해당 line을 수정하지 말 것 ***
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1,
bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.GELU()
# Residual Blocks
self.layer1 = self._make_layer(64, 64, blocks=2, stride=1)
self.layer2 = self._make_layer(64, 128, blocks=2, stride=2)
self.layer3 = self._make_layer(128, 256, blocks=2, stride=2)
self.layer4 = self._make_layer(256, 512, blocks=2, stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512, 100) # CIFAR-100은 100개의 클래스
def _make_layer(self, in_channels, out_channels, blocks, stride):
layers = []
layers.append(BasicBlock(in_channels, out_channels, stride))
for _ in range(1, blocks):
layers.append(BasicBlock(out_channels, out_channels, stride=1))
return nn.Sequential(*layers)
def forward(self, x):
# TODO: forward pass 정의
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(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
model = CNN().cuda()
모델의 아키텍처는 기본적으로 초기 Convolution 레이어와 네 개의 Residual 블록으로 구성되어 있다. 초기 Conv 레이어에서는 3채널의 입력 이미지를 받아 64채널의 특징 맵으로 변환한다. 이 과정에서 커널 크기는 3x3, 스트라이드는 1, 패딩은 1로 설정되었으며, Batch Normalization와 GELU 활성화 함수를 적용하여 초기 특징 추출을 수행한다.
그 이후로는 네 개의 잔차 블록인 Layer1부터 Layer4까지 진행된다. 각 레이어는 여러 개의 ResNet의 BasicBlock으로 구성되어 있으며, 다음과 같은 특징을 갖는다.
Layer1: 입력 채널 64 → 출력 채널 64, 스트라이드 1, BasicBlock 2개
Layer2: 입력 채널 64 → 출력 채널 128, 스트라이드 2, BasicBlock 2개
Layer3: 입력 채널 128 → 출력 채널 256, 스트라이드 2, BasicBlock 2개
Layer4: 입력 채널 256 → 출력 채널 512, 스트라이드 2, BasicBlock 2개
각 BasicBlock은 두 개의 3x3 합성곱 레이어로 구성되어 있으며, 각 레이어 뒤에는 배치 정규화와 GELU 활성화 함수가 적용됨. 중요한 것은 Residual Connection 을 통해 블록의 입력을 출력과 더해줌으로써 깊은 네트워크에서도 gradient vanishing 문제를 완화하였다.
만약 입력과 출력의 차원이 다르거나 stride가 2인 경우, 1x1 합성곱을 사용하는 shortcut을 통해 차원을 맞추어 Residual 연결을 구현하였다.
특징 맵의 공간적 크기는 각 레이어를 거치면서 stride가 2인 합성곱 레이어에 의해 점진적으로 감소. 이는 이미지의 고차원 특징을 추출하면서도 계산 효율성을 높인다. 마지막으로, Adaptive Average Pooling을 통해 특징 맵의 크기를 1x1로 축소하고, 이를 펼쳐서 512차원의 벡터로 변환한 후, fully-connected layer을 통해 100개의 클래스에 대한 예측을 수행한다.
# TODO: model 명의로 생성된 CNN 모델에 대해 학습 수행하기
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=1e-4)
num_epochs = 30
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs)
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
for images, labels in train_loader:
images = images.cuda()
labels = labels.cuda()
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
scheduler.step()
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}')
Epoch [1/30], Loss: 3.7992
Epoch [2/30], Loss: 3.1445
Epoch [3/30], Loss: 2.6234
Epoch [4/30], Loss: 2.2507
Epoch [5/30], Loss: 1.9566
Epoch [6/30], Loss: 1.7302
Epoch [7/30], Loss: 1.5461
Epoch [8/30], Loss: 1.3828
Epoch [9/30], Loss: 1.2528
Epoch [10/30], Loss: 1.1343
Epoch [11/30], Loss: 1.0273
Epoch [12/30], Loss: 0.9275
Epoch [13/30], Loss: 0.8290
Epoch [14/30], Loss: 0.7397
Epoch [15/30], Loss: 0.6589
Epoch [16/30], Loss: 0.5749
Epoch [17/30], Loss: 0.4979
Epoch [18/30], Loss: 0.4228
Epoch [19/30], Loss: 0.3618
Epoch [20/30], Loss: 0.3024
Epoch [21/30], Loss: 0.2544
Epoch [22/30], Loss: 0.2090
Epoch [23/30], Loss: 0.1752
Epoch [24/30], Loss: 0.1479
Epoch [25/30], Loss: 0.1328
Epoch [26/30], Loss: 0.1153
Epoch [27/30], Loss: 0.1069
Epoch [28/30], Loss: 0.1011
Epoch [29/30], Loss: 0.0959
Epoch [30/30], Loss: 0.0924
학습 전략 측면에서, Optimizer로는 AdamW를 사용하여 weight decay를 통해 모델의 일반화 능력을 향상시켰다. 초기 학습률은 0.001로 설정하였으며, Cosine Annealing Scheduler을 통해 학습률을 동적으로 조정
총 30 epochs 동안 학습을 진행하였고, 배치 크기는 512로 설정하여 학습의 효율성과 안정성을 높였다. 손실 함수로는 교차 엔트로피 손실(CrossEntropyLoss)을 사용하여 다중 클래스 분류 문제에 적합한 학습을 수행하였다.
# 학습된 모델 평가 *** 해당 cell을 수정하지 말 것 ***
test_dataset = datasets.CIFAR100(root='.', train=False, download=True,
transform=transforms.ToTensor())
batchsize = 64
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batchsize, shuffle=True)
num_correct = 0
model.eval()
with torch.no_grad():
for image, label in test_loader:
image = image.cuda()
label = label.cuda()
output = model(image)
pred = output.argmax(dim=1)
num_correct += (pred == label).sum()
print(f'Accuracy : {num_correct / len(test_dataset) * 100:.2f} %')
Accuracy : 70.81 %
이러한 구조적 특징과 학습 전략의 조합을 통해 모델은 CIFAR-100 데이터셋에서 70.81%라는 높은 테스트 정확도를 달성할 수 있었다. 특히, MLP project에 이어 ResNet의 핵심인 Residual connection을 도입함으로써 깊은 네트워크에서도 효과적인 학습이 가능하였다.
또한 ResNet인 만큼 Convolution과 같이 시너지가 형성되어 성능이 확실히 향상된 느낌이 들었다.
ResNet은 강력한 구조임을 또 한번 알게 되었다.
...다음에는 WSML, Weakly Supervised Multi-Label 논문을 몇 개 더 다루어보겠다
'Machine Learning' 카테고리의 다른 글
컨볼루션 신경망(CNN)(2) (0) | 2024.02.23 |
---|---|
컨볼루션 신경망(CNN)(1) (0) | 2024.02.14 |
딥러닝 구현 및 시각화 해보기 (1) | 2024.02.14 |
딥러닝 다층화에 의한 문제점 및 구현에 필요한 과정 (1) | 2024.02.11 |
역전파 구현 및 시각화 해보기 (0) | 2024.02.11 |