본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 인공지능 고급-시각 강의의 CNN알고리즘 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.
VGGNet은 옥스퍼드 대학교의 Visual Geometry Group의 Andrew Zisserman, Karen Simonyan등이 개발한 아키텍쳐로, 논문의 주 기여로는 Factorizing convolution 개념의 도입을 통해 네트워크의 복잡도를 낮추면서, 계산효율성은 높이고, 나아가 네트워크의 깊이를 깊게 만드는 방법론과 그 효용을 드러낸 부분이라 할 수 있다.
Factorizing convolution은 위 사진처럼
7x7의 입력 feature을
3x3의 출력 feature로 합성곱 연산을 할 때
5x5 커널과 같이 큰 커널을 1회 적용하는 것보단
3x3 커널을 작은 커널을 2회 적용하는 것이 더 효과적
이라는 설명이다.
전체 파라미터의 개수를 본다면 큰 커널 은 25개의 파라미터가 발생하지만
작은 커널을 다회 적용하더라도 파라미터의 개수는 18개로 더 적게 발생하는 것을 알 수 있다.
이같이 파라미터의 개수가 줄어듬은 자연스레 연산량의 감소를 꾀할 수 있어 네트워크가 더 가볍고 빠르게 학습이 가능해지고,
절감시킨 연산량을 그대로 네트워크의 깊이를 늘리는데 사용함으로써 더 다양한 특징에 대한 학습을 가능하게 해
결과적으로 연산의 효율성 + 학습성능의 향상 두가지 이점을 얻어낸 방법론이라 볼 수 있다.
또한 커널 사이즈를 [3x3]으로 고정하고 네트워크 전체를 몇개의 Sequential block로 나눈 뒤 각 블록 별 Conv의 반복 횟수에 차이를 두는 식으로 네트워크 전체 구조는 비슷하면서도 Depth에 차이가 있는 파생 네트워크를 쉽게 설계하는 기법을 소개하고 있다.
따라서 이 이후의 CNN 계열 논문은 네트워크 구조 전체에 영향력을 끼치는 규칙을 정하고
그 아래 세부 조정 인자를 생성해 이를 통해 파생 네트워크를 설계하는 식으로 사용 목적에 적합한 네트워크 리스트를 상호 비교 및 제안 하는식으로 논문을 작성하는 것이 하나의 트랜드가 된다.
VGG 네트워크의 아키텍쳐는 위 사진처럼 [224x224x3]의 입력을 받으며,
Conv layer의 반복 횟수에 따라 vgg-16, 19로 나뉜다.
이를 그림으로 그려본다면
이와 같이 5개의 Conv block와 1개의 FC block로 구성되어 있고, Conv layer의 Kernel size는 [3x3], stride=1, padding=1 로 고정하여, 같은 레이어를 반복해서 쌓아 그 깊이를 깊게 만드는데 유리하도록 인자값을 설정했다.
이를 코드로 구현하면 아래와 같다.
import torch
import torch.nn as nn
class VGG19(nn.Module):
def __init__(self, in_channels=3, num_classes=10):
super(VGG19, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.conv2 = nn.Sequential(
nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.conv3 = nn.Sequential(
nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.conv4 = nn.Sequential(
nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.conv5 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.flatten = nn.Flatten()
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.conv5(x)
x = self.flatten(x)
x = self.classifier(x)
return x
from torchsummary import summary #설계한 모델의 요약본 출력 모듈
model = VGG19()
summary(model, input_size=(3, 224, 224), device='cpu')
모델을 GPU로 넘기는거 잊지말자
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(device)
model = VGG19()
model.to(device)
summary(model, input_size=(3, 224, 224), device=device.type)
----------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Conv2d-1 [-1, 64, 224, 224] 1,792
ReLU-2 [-1, 64, 224, 224] 0
Conv2d-3 [-1, 64, 224, 224] 36,928
ReLU-4 [-1, 64, 224, 224] 0
MaxPool2d-5 [-1, 64, 112, 112] 0
Conv2d-6 [-1, 128, 112, 112] 73,856
ReLU-7 [-1, 128, 112, 112] 0
Conv2d-8 [-1, 128, 112, 112] 147,584
ReLU-9 [-1, 128, 112, 112] 0
MaxPool2d-10 [-1, 128, 56, 56] 0
Conv2d-11 [-1, 256, 56, 56] 295,168
ReLU-12 [-1, 256, 56, 56] 0
Conv2d-13 [-1, 256, 56, 56] 590,080
ReLU-14 [-1, 256, 56, 56] 0
Conv2d-15 [-1, 256, 56, 56] 590,080
ReLU-16 [-1, 256, 56, 56] 0
Conv2d-17 [-1, 256, 56, 56] 590,080
ReLU-18 [-1, 256, 56, 56] 0
MaxPool2d-19 [-1, 256, 28, 28] 0
Conv2d-20 [-1, 512, 28, 28] 1,180,160
ReLU-21 [-1, 512, 28, 28] 0
Conv2d-22 [-1, 512, 28, 28] 2,359,808
ReLU-23 [-1, 512, 28, 28] 0
Conv2d-24 [-1, 512, 28, 28] 2,359,808
ReLU-25 [-1, 512, 28, 28] 0
Conv2d-26 [-1, 512, 28, 28] 2,359,808
ReLU-27 [-1, 512, 28, 28] 0
MaxPool2d-28 [-1, 512, 14, 14] 0
Conv2d-29 [-1, 512, 14, 14] 2,359,808
ReLU-30 [-1, 512, 14, 14] 0
Conv2d-31 [-1, 512, 14, 14] 2,359,808
ReLU-32 [-1, 512, 14, 14] 0
Conv2d-33 [-1, 512, 14, 14] 2,359,808
ReLU-34 [-1, 512, 14, 14] 0
Conv2d-35 [-1, 512, 14, 14] 2,359,808
ReLU-36 [-1, 512, 14, 14] 0
MaxPool2d-37 [-1, 512, 7, 7] 0
Flatten-38 [-1, 25088] 0
Linear-39 [-1, 4096] 102,764,544
ReLU-40 [-1, 4096] 0
Dropout-41 [-1, 4096] 0
Linear-42 [-1, 4096] 16,781,312
ReLU-43 [-1, 4096] 0
Dropout-44 [-1, 4096] 0
Linear-45 [-1, 1000] 4,097,000
================================================================
Total params: 143,667,240
Trainable params: 143,667,240
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 238.69
Params size (MB): 548.05
Estimated Total Size (MB): 787.31
----------------------------------------------------------------
논문이 작성된 시점에서 VGGNet는 ImageNet
데이터 셋에 대한 성능평가를 목적으로 했기에 ImageNet
의 class개수인 1000으로 모델을 설계하면 대략 1억4천만개의 파라미터가 생성됨을 확인할 수 있다.
이번에는 CIFAR10
데이터셋과 유사하지만 label class가 100개인 CIFAR100
데이터셋을 사용하고자 한다.
from torchvision import datasets
trainset = datasets.CIFAR100(root=[파일경로] -> 귀찮으면`./data`,
train=True,
download=True)
testset = datasets.CIFAR100(root=[파일경로] -> 귀찮으면`./data`,
train=False,
download=True)
# 학습 이미지 개수와 고유 라벨 개수 추출
num_train_images = len(trainset)
unique_train_labels = len(set(trainset.targets))
num_test_images = len(testset)
unique_test_labels = len(set(testset.targets))
print(f"Training Image : {num_train_images}, Unique Labels : {unique_train_labels}")
print(f"Testing Image : {num_test_images}, Unique Labels : {unique_test_labels}")
Files already downloaded and verified
Files already downloaded and verified
Training Image : 50000, Unique Labels : 100
Testing Image : 10000, Unique Labels : 100
VGG19가 발표된 당시에는 '데이터 증강(Data Argumentation)'기법이 적용되었고, 적용된 기법에 대한 설명은 나중에 하고 일단 기법에 대한 코드만 적용시켜 보도록 하겠다.
from torchvision.transforms import v2
from torch.utils.data import DataLoader
#CIFAR100의 데이터 정규화를 위한 mean, std 설정값
CIFAR_100_val = ((0.5074,0.4867,0.4411),(0.2011,0.1987,0.2025))
# 이미지 전처리
train_transformation = v2.Compose([
#훈련데이터용 데이터 증강기법
v2.RandomResizedCrop((224,224)),
v2.RandomHorizontalFlip(p=0.5),
v2.ColorJitter(brightness=0.4,
contrast=0.4,
saturation=0.4,
hue=0.1),
v2.Resize((224, 224)), #이미지 크기를 224x224로
v2.ToImage(), # 이미지를 Tensor 자료형으로 변환
v2.ToDtype(torch.float32, scale=True), #텐서 자료형을 [0~1]로 스케일링
v2.Normalize(mean=CIFAR_100_val[0], std=CIFAR_100_val[1])
])
test_transforamtion = v2.Compose([
v2.Resize((224, 224)), #이미지 크기를 224x224로
v2.ToImage(), # 이미지를 Tensor 자료형으로 변환
v2.ToDtype(torch.float32, scale=True), #텐서 자료형을 [0~1]로 스케일링
v2.Normalize(mean=CIFAR_100_val[0], std=CIFAR_100_val[1])
])
#전처리 방법론 적용
trainset.transform = train_transformation
testset.transform = test_transforamtion
BATCH_SIZE = 128
#데이터로더 생성
train_loader = DataLoader(trainset, batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(testset, batch_size=BATCH_SIZE, shuffle=False)
일단 VGG19모델을 GPU에서 학습시킬 것이고, CIFAR100
의 클래스 개수에 맞춰 다시 아키텍쳐 초기화를 수행한다.
from torchsummary import summary
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = VGG19(num_classes=100)
model.to(device)
summary(model, input_size=(3, 224, 224), device=device.type)
VGG19가 발표된 당시 논문의 하이퍼 파라미터는 아래와 같다.
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
하지만 실습에 사용한 손실함수, 옵티마이저, 스케쥴러는 아래와 같이 설정했다.
from torch import optim
#LossFn, Optimizer, scheduler 정의
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=200)
from tqdm import tqdm #훈련 진행상황 체크
#tqdm 시각화 도구 출력 사이즈 조절 변수
epoch_step = 5
def model_train(model, data_loader,
loss_fn, optimizer_fn, scheduler_fn,
processing_device, epoch):
model.train() # 모델을 훈련 모드로 설정
global epoch_step
# loss와 accuracy를 계산하기 위한 임시 변수를 생성
run_size, run_loss, correct = 0, 0, 0
# 특정 에폭일 때만 tqdm 진행상황 바 생성
if (epoch + 1) % epoch_step == 0 or epoch == 0:
progress_bar = tqdm(data_loader)
else:
progress_bar = data_loader
for image, label in progress_bar:
# 입력된 데이터를 먼저 GPU로 이전하기
image = image.to(processing_device)
label = label.to(processing_device)
# 전사 과정 수행
output = model(image)
loss = loss_fn(output, label)
#backward과정 수행
optimizer_fn.zero_grad()
loss.backward()
optimizer_fn.step()
# 스케줄러 업데이트
scheduler_fn.step()
#argmax = 주어진 차원에서 가장 큰 값을 가지는 요소의 인덱스를 반환
pred = output.argmax(dim=1) #예측값의 idx출력
correct += pred.eq(label).sum().item()
#현재까지 수행한 loss값을 얻어냄
run_loss += loss.item() * image.size(0)
run_size += image.size(0)
#tqdm bar에 추가 정보 기입
if (epoch + 1) % epoch_step == 0 or epoch == 0:
progress_bar.set_description('[Training] loss: ' +
f'{run_loss / run_size:.4f}, accuracy: ' +
f'{correct / run_size:.4f}')
avg_accuracy = correct / len(data_loader.dataset)
avg_loss = run_loss / len(data_loader.dataset)
return avg_loss, avg_accuracy
def model_evaluate(model, data_loader, loss_fn,
processing_device, epoch):
model.eval() # 모델을 평가 모드로 전환 -> dropout 기능이 꺼진다
# batchnormalizetion 기능이 꺼진다.
global epoch_step
# gradient 업데이트를 방지해주자
with torch.no_grad():
# 여기서도 loss, accuracy 계산을 위한 임시 변수 선언
run_loss, correct = 0, 0
# 특정 에폭일 때만 tqdm 진행상황 바 생성
if (epoch + 1) % epoch_step == 0 or epoch == 0:
progress_bar = tqdm(data_loader)
else:
progress_bar = data_loader
for image, label in progress_bar: # 이때 사용되는 데이터는 평가용 데이터
# 입력된 데이터를 먼저 GPU로 이전하기
image = image.to(processing_device)
label = label.to(processing_device)
# 평가 결과를 도출하자
output = model(image)
pred = output.argmax(dim=1) #예측값의 idx출력
# 모델의 평가 결과 도출 부분
# 배치의 실제 크기에 맞추어 정확도와 손실을 계산
correct += torch.sum(pred.eq(label)).item()
run_loss += loss_fn(output, label).item() * image.size(0)
accuracy = correct / len(data_loader.dataset)
loss = run_loss / len(data_loader.dataset)
return loss, accuracy
# 학습과 검증 손실 및 정확도를 저장할 리스트
his_loss, his_accuracy = [], []
num_epoch = 50
for epoch in range(num_epoch):
# 훈련 손실과 정확도를 반환 받습니다.
train_loss, train_acc = model_train(model, train_loader,
criterion, optimizer, scheduler,
device, epoch)
# 검증 손실과 검증 정확도를 반환 받습니다.
test_loss, test_acc = model_evaluate(model, test_loader,
criterion, device, epoch)
# 손실과 정확도를 리스트에 저장
his_loss.append((train_loss, test_loss))
his_accuracy.append((train_acc, test_acc))
#epoch가 특정 배수일 때만 출력하기
if (epoch + 1) % epoch_step == 0 or epoch == 0:
print(f"epoch {epoch+1:03d}, Training loss: " +
f"{train_loss:.4f}, Training accuracy: {train_acc:.4f}")
print(f"Test loss: {test_loss:.4f}, Test accuracy: {test_acc:.4f}")
자 여기까지 하고 학습/검증 결과를 확인해 본다면 처참한 성능이 나왔을 것이다.
학습/검증의 결과가 조금 만족스럽지는 못하더라도
epoch가 진행될 수록
Loss는 좌 하향
Accuracy는 우 상향 그래프가 나와줘야 정상이다.
근데 이건... 심폐소생술이 필요한 그래프가 나와버렸다.
아에 학습이 진행이 안된 것인데 조언을 구해보니
필자의 Local PC의 성능으로는 VGG 19
를 학습시키기에는 너무 성능이 낮기에 파라미터 개수를 조정해야 함을 확인 할 수 있었다.
진짜 이런 감정이었다...
우선 로컬 PC에서 VGGNet를 학습시키기 위한 최적의 세팅을 수행해야 했다.
해결법을 요약하자면
1) Input img
의 크기를 축소하여 입력 ->
2) epoch
횟수 늘리기
3) 훈련시간 조정을 위해 Batch_size
재설정
등을 수행했다.
# 이미지 전처리
train_transformation = v2.Compose([
v2.RandomResizedCrop((224,224)),
v2.RandomHorizontalFlip(p=0.5),
v2.ColorJitter(brightness=0.4,
contrast=0.4,
saturation=0.4,
hue=0.1),
v2.Resize((96, 96)), #이미지 크기를 224x224로
v2.ToImage(), # 이미지를 Tensor 자료형으로 변환
v2.ToDtype(torch.float32, scale=True), #텐서 자료형을 [0~1]로 스케일링
v2.Normalize(mean=CIFAR_100_val[0], std=CIFAR_100_val[1])
])
test_transforamtion = v2.Compose([
v2.Resize((96, 96)), #이미지 크기를 224x224로
v2.ToImage(), # 이미지를 Tensor 자료형으로 변환
v2.ToDtype(torch.float32, scale=True), #텐서 자료형을 [0~1]로 스케일링
v2.Normalize(mean=CIFAR_100_val[0], std=CIFAR_100_val[1])
])
이미지의 크기 조절은 데이터 전처리부에서
v2.Resize((224, 224)),
부분을
v2.Resize((96, 96)),
으로 조정한 뒤
VGG19 아키텍쳐 설계 클래스 부의
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(4096, num_classes),
)
nn.Linear(512 * 7 * 7, 4096),
을
nn.Linear(512 * 3 * 3, 4096),
으로
FCL로 넘어갈 때의 축소된 입력 이미지에 맞춰 차원값을 계산하고 이를 조정하였다.
이렇게 입력이미지의 크기를 줄이면 그에 맞춰 사용되는 파라미터의 개수가 줄어들게 된다.
대략 1억4천만개에서 5천6백만개로 줄었으니 많이 줄인 것이라 보면 된다.
다음으로
num_epoch = 100
for epoch in range(num_epoch):
훈련/검증 반복 횟수(epoch
)는 2배로 늘렸으며,
파라미터 개수를 줄였으니 GPU메모리 할당에 여유가 생겨 더 빠르게 학습을 시키기 위해 Batch_size
를 조정하였다.
BATCH_SIZE = 512
train_loader = DataLoader(trainset, batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(testset, batch_size=BATCH_SIZE, shuffle=False)
이런 식으로 로컬PC나 colab에서 공부용으로 딥러닝 아키텍쳐의 학습을 진행할 수 있고
그 성능결과를 확인할 수 있다.
이렇게 아키텍쳐가 무거워서 로컬 PC에서는 감당이 어려운 네트워크의 경우
전이학습, 미세조정 기법(Fine Turning)등의 방법론을 도입하여 로컬 PC에서도 네트워크의 훈련/검증/추론 단계를 수행할 수 있지만
이같은 내용은 다음에 포스팅 하도록 하겠다.