이번에는 VGG Net을 실제로 PyTorch로 구현해 보고 성능까지 검증해보도록 하겠습니다.
from google.colab import drive import os import sys drive.flush_and_unmount() drive.mount('/content/drive/') sys.path.append("/content/drive/MyDrive/DeepLearning")
먼저 위와같이 colab과 google 드라이브를 마운트 해줍니다.
''' 필요 라이브러리들을 불러온다. ''' import copy import numpy as np import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import torch.utils.data as data import torchvision import torchvision.transforms as transforms import torchvision.datasets as Datasets device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
그리고 관련 라이브러리들을 싹다 불러옵니다.
class VGG(nn.Module): def __init__(self, features, output_dim): super().__init__() self.features = features self.avgpool = nn.AdaptiveAvgPool2d(7) # 입력 텐서가 7x7로 평균 폴링된다 (폴링 영역 크기, 스트라이드는 자동계산된다. self.classifier = nn.Sequential( # fully-connected 레이어를 정의한다. nn.Linear(512*7*7, 4096), nn.ReLU(inplace=True), # 메모리를 아까기 위해 inplace로 연산을 수행한다. nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(4096, output_dim), # 출력층 정의 ) def forward(self, x): x = self.features(x) x = self.avgpool(x) h = x.view(x.shape[0], -1) # 미니배치 빼고 flattening을 해서 fc에 넣을 준비를 함 x = self.classifier(h) return x, h
그 후, 논문에 있는 그대로를 nn.Module을 통해 옮깁니다. forward pass에서는 최종적으로 feature -> avgpool -> fc -> softmax 이런식으로 거쳐가게 될 것입니다. 그 다음은 모델 정의입니다.
''' 여기서 숫자(output channel)은 Conv2d를 수행하라는 의미이다. -> 출력채널은 다음 레이어의 입력 채널(input channel)이 된다. M은 최대 폴링(max pooling)을 수행하라는 것이다. ''' vgg11_config = [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'] vgg13_config = [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'] # 13(Conv) + 3(Pooling) = 16 = VGG16 vgg16_config = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'] vgg19_config = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M']
주석 그대로입니다. 저희는 간단히 vgg11_config만을 활용해서 실습할 것입니다.
def get_vgg_layers(config, batch_norm): layers = [] in_channels = 3 for c in config: assert c == 'M'or isinstance(c, int) # assert문으로 값의 무결성을 체크한다. if c == 'M': layers += [nn.MaxPool2d(kernel_size=2)] # maxpolling의 kernel-size는 논문에 2x2라고 되어 있음 else: conv2d = nn.Conv2d(in_channels, c, kernel_size=3, padding=1) # kernel_size=3x3, zerro_pading, in, out channel 정의 if batch_norm: # Batch Normalization(BN) 을 적용할 경우 BN + ReLU 적용 layers += [conv2d, nn.BatchNorm2d(c), nn.ReLU(inplace=True)] else: layers += [conv2d, nn.ReLU(inplace=True)] in_channels = c return nn.Sequential(*layers) # 네트워크의 모든 계층을 반환한다.
그 다음 vgg_layer를 위 config를 통해 batch_norm 인수와 함께 생성할 수 있는 함수를 정의합니다.
vgg11_layers = get_vgg_layers(vgg11_config, batch_norm=True) print(vgg11_layers)

우리가 원하던 대로 피처가 잘 생성된 것을 볼 수 있습니다. 그 다음은 이제 모델 전체에 대한 네트워크를 확인해 보도록 하겠습니다.
OUTPUT_DIM = 2 # 개와 고양이 두 개의 클래스를 사용 model = VGG(vgg11_layers, OUTPUT_DIM) print(model)

VGG11이 훌륭하게 구성되었습니다. 그 외에도 Pre-trained VGG11 모델을 torchvision에서 제공합니다.
import torchvision.models as models ''' vgg11_bn은 VGG11 기본 모델에 배치 정규화가 적용된 모델을 사용하겠다는 의미 -> pretrained=True (사전 학습된 모델을 사용하겠다.) ''' pretrained_model = models.vgg11_bn(pretrained=True) print(pretrained_model)
이를 사용해도 좋지만, 우리는 우리가 정의한 모델을 가지고, 학습을 시켜서 직접 테스트 까지 해보도록 하겠습니다.
train_transforms = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomRotation(5), transforms.RandomHorizontalFlip(0.5), transforms.ToTensor(), # 각 채널별로 이미지의 픽셀 값을 정규화하는 것이다. ex) Red -> (pixel_value - 0.485) / 0.229 # ImageNet 데이터셋으로 사전학습된 모델을 사용할 경우 이러한 정규화를 사용하는 것이 일반적 transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) test_transforms = transforms.Compose([ transforms.Resize((256, 256)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])])
먼저 ImageNet 데이터를 위한 전처리를 해줍니다. istropically-rescale, random rotation, horizontal flip, normalize를 해줍니다. 이는 train에서 입력의 일반화 성능을 높이고자 하는 것입니다.
train_path = './data/catanddog/train' test_path = './data/catanddog/test' train_dataset = torchvision.datasets.ImageFolder( train_path, transform=train_transforms ) test_dataset = torchvision.datasets.ImageFolder( test_path, transform=test_transforms ) print(len(train_dataset)), print(len(test_dataset))

그리고 ImageFolder를 이용해서 모델 학습에 필요한 데이터 셋을 가져옵니다. 우리가 정의한 train_dataset, test_dataset의 크기는 위와 같다는걸 확인할 수 있습니다. 그 다음으로는 훈련과 검증 데이터를 분할해보겠습니다.
VALID_RATIO = 0.9 n_train_examples = int(len(train_dataset) * VALID_RATIO) n_valid_examples = len(train_dataset) - n_train_examples train_data, valid_data =\ data.random_split(train_dataset, [n_train_examples, n_valid_examples])
그 다음으로는 검증 데이터 전처리를 해주겠습니다.
valid_data = copy.deepcopy(valid_data) valid_data.dataset.transform = test_transforms

그 다음에는 훈련, 검증, 테스트로 분류한 데이터의 길이를 확인해보았습니다. 이제는 이를 가지고 Dataloader를 batch_size를 통해 정의합니다.
BATCH_SIZE = 100 # 훈련 데이터만 임의로 섞어서 가져와야 한다. train_iterator = data.DataLoader(train_data, shuffle=True, batch_size=BATCH_SIZE) valid_iterator = data.DataLoader(valid_data, batch_size=BATCH_SIZE) test_iterator = data.DataLoader(test_dataset, batch_size=BATCH_SIZE)
그 다음으로는 이제 옵티마이저와 손실함수를 정의합니다.
optimizer = optim.Adam(model.parameters(), lr=1e-7) criterion = nn.CrossEntropyLoss() model = model.to(device) criterion = criterion.to(device) print(optimizer)
옵티마이저로는 논문에 나온 대로 Adam, 손실함수로는 다중 분류문제를 진행할 것이므로, CrossEntropyLoss를 사용해 주도록 하겠습니다. 그리고 model, criterion을 GPU에 부착해줍니다!
def calculate_accuracy(y_pred, y): top_pred = y_pred.argmax(1, keepdim=True) # 입력된 텐서에서 각 행을 따라 max의 인덱스를 반환 # 이는 y에 대한 텐서를 top_pred.size()으로 보겠다는 의미 -> 비교한다음에 개수를 센다! correct = top_pred.eq(y.view_as(top_pred)).sum() acc = correct.float() / y.shape[0] return acc
그리고 위처럼 모델의 정확도를 측정하기 위한 함수를 정의해줍니다.
def train(model, iterator, optimizer, criterion, device): epoch_loss = 0 epoch_acc = 0 for (x, y) in iterator: x = x.to(device) y = y.to(device) model.train() optimizer.zero_grad() y_pred, _ = model(x) # forward pass loss = criterion(y_pred, y) # cross entropy loss acc = calculate_accuracy(y_pred, y) loss.backward() optimizer.step() epoch_loss += loss.item() epoch_acc += acc.item() return epoch_loss / len(iterator), epoch_acc / len(iterator)
그리고 훈련 데이터 셋을 이용할 모델 학습 함수를 저으이해 주었습니다. 각 epoch 마다의 loss, acc를 반환해주게 했습니다. 검증도 비슷하게 짜주었습니다.
def evaluate(model, iterator, criterion, device): epoch_loss = 0 epoch_acc = 0 model.eval() with torch.no_grad(): for (x, y) in iterator: x = x.to(device) y = y.to(device) y_pred, _ = model(x) loss = criterion(y_pred, y) acc = calculate_accuracy(y_pred, y) epoch_loss += loss.item() epoch_acc += acc.item() return epoch_loss / len(iterator), epoch_acc / len(iterator)
여기서는 당연하지만 가중치 갱신을 하면 안됩니다. torch.no_grad를 이용해 주었습니다. 그리고 이제 학습 시간을 측정해보기 위해 함수를 하나 정의해주었습니다.
import time def epoch_time(start_time, end_time): elapsed_time = end_time - start_time elapsed_mins = int(elapsed_time / 60) elapsed_secs = int(elapsed_time - (elapsed_mins * 60)) return elapsed_mins, elapsed_secs
이제 모델 학습 시작합니다!
import torch, gc gc.collect() torch.cuda.empty_cache()
torch gc 초기화 한번 해주고~
EPOCHS = 5 best_valid_loss = float('inf') for epoch in range(EPOCHS): start_time = time.monotonic() train_loss, train_acc = train(model, train_iterator, optimizer, criterion, device) valid_loss, valid_acc = evaluate(model, valid_iterator, criterion, device) ''' valid_loss가 가장 작은 값을 구하고 그 상태의 모델을 VGG-model.pt 이름으로 저장한다. ''' if valid_loss < best_valid_loss: best_valid_loss = valid_loss torch.save(model.state_dict(), './data/VGG-model.pt') end_time = time.monotonic() epoch_mins, epoch_secs = epoch_time(start_time, end_time) print(f'Epoch: {epoch+1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s') print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%') print(f'\t Valid. Loss: {valid_loss:.3f} | Valid. Acc: {valid_acc*100:.2f}%')
총 5번의 에폭을 돌면서 우리가 필요한 값들을 찍어보았습니다. vgg11이고 colab의 GPU를 사용중이기 때문에 학습하는데 시간이 많이 안걸릴 것으로 예상했습니다.

위 코드에서 best_valid_loss인 시점의 모델을 ./data/VGG_model.pt의 경로에 저장했습니다. 이제 이를 통해 테스트 데이터 셋에 대한 성능을 측정해보겠습니다.
model.load_state_dict(torch.load('./data/VGG-model.pt')) test_loss, test_acc = evaluate(model, test_iterator, criterion, device) print(f'Test Loss: ${test_loss: .3f} | Test Acc: {test_acc*100: .2f}%')

성능이 썩 그리 좋지는 못하네요,,, 아마 데이터 개수가 너무 적어서 그런거 같습니다. 이제는 직접 테스트 데이터 셋을 이용한 모델의 예측 결과를 알아보기 위한 함수를 정의해주겠습니다.
def get_prediction(model, iterator): model.eval() images = [] labels = [] probs = [] with torch.no_grad(): for (x, y) in iterator: x = x.to(device) y_pred, _ = model(x) y_prob = F.softmax(y_pred, dim=1) top_pred = y_prob.argmax(1, keepdim=True) images.append(x.cpu()) # detach한다. labels.append(y.cpu()) probs.append(y_prob.cpu()) images = torch.cat(images, dim=0) labels = torch.cat(labels, dim=0) probs = torch.cat(probs, dim=0) return images, labels, probs
테스트를 돌면서, image, labels, probs를 torch.cat을 이용해서 붙여주었습니다. 그리고 우리는 예측 중에서 정확하게 예측한 것을 추출해보겠습니다.
images, labels, probs = get_prediction(model, test_iterator) pred_labels = torch.argmax(probs, 1) corrects = torch.eq(labels, pred_labels) # 예측과 정답이 같은지 비교 correct_examples = [] for image, label, prob, correct in zip(images, labels, probs, corrects): if correct: correct_examples.append((image, label, prob)) correct_examples.sort(reverse=True, key=lambda x: torch.max(x[2], dim=0).values) print(correct_examples[0], correct_examples[0][2].shape)

대충 우리가 원하는대로 결과가 나왔네요, 위 결과는, (image, label, prob) 이런 식으로 맞은 데이터 중에 첫번째만 가져온 것입니다. 그리고 우리는 dog or cat으로 분류할 것인데, 강아지(1), 고양이(2) 이 순서대로 뽑아내고 싶어서 sort를 통해 정렬도 해주었습니다.
그리고 이미지를 matplotlib로 그려볼거기 때문에, 이미지 출력을 위한 전처리 과정도 진행해 주어야 합니다.
def normalize_image(image): image_min = image.min() image_max = image.max() image.clamp_(min=image_min, max=image_max) image.add_(-image_min).div_(image_max - image_min + 1e-5) return image
우리가 위에서 이미지에 대한 전처리를 transform을 통해 일반화 성능을 높이기 위해 많이 바꿨었습니다. 본래 이미지를 출력하기 위해서, 위와같이 정규화를 진행해주었습니다. 각 픽셀 값을 다시 [0, 1]로 재설정해주었습니다.

위는 우리가 ImageFolder를 통해 불러온 폴더의 구조입니다! 언급을 안한거 같아서요
import matplotlib.pyplot as plt def plot_most_correct(correct, classes, n_images, normalize=True): rows = int(np.sqrt(n_images)) cols = int(np.sqrt(n_images)) fig = plt.figure(figsize=(25, 10)) for i in range(rows*cols): ax = fig.add_subplot(rows, cols, i+1) # 출력하는 그래프 개수만큼 subplot을 만들어 준다. image, true_label, probs = correct[i] image = image.permute(1, 2, 0) true_prob = probs[true_label] correct_prob, correct_label = torch.max(probs, dim=0) true_class = classes[true_label] correct_class = classes[correct_label] if normalize: image = normalize_image(image) ax.imshow(image.cpu().numpy()) ax.set_title(f'true label: {true_class} ({true_prob: .3f})\n' \ f'pre label: {correct_class} ({correct_prob: .3f})') ax.axis('off') fig.subplots_adjust(hspace=0.4)
그 다음 우리가 추출한 correct_examples을 가지고 간단히 그림을 그려보겠습니다. 여기에는 true_class와 true_prob를 통해 실제 정답에 대한 정보, correct_class, correct_prob을 통해 우리가 추측한 정보를와 해당하는 정규화한 그림을 뽑아냈습니다.

생각보다 너무 성능이 좋지 않았습니다. 이 또한 데이터 셋을 늘려주면 성능이 늘어날 것으로 기대합니다.