PyTorch에서 VGG from Scratch 쓰기

내가 지난 1-2개 십년 동안 컴퓨터 비전 분야를 혁신시킨 经典 卷积神经网络에 관한 시리즈를 계속하며, 다음으로 PyTorch를 사용하여 처음부터 매우 깊은 卷积神经网络인 VGG를 구축하겠습니다. 시리즈의 이전 기사를 내 프로필에서 확인할 수 있으며, 주로 LeNet5AlexNet입니다.

지금까지와 마찬가지로 VGG의 아키텍처와 그 배경을 살펴볼 것이며, 당시 결과가 어떠한지 알아보겠습니다. 그 다음에 우리의 데이터셋인 CIFAR100을 살펴보고, 메모리 효율적인 코드를 사용하여 프로그램에 로드합니다. 그런 다음 PyTorch를 사용하여 VGG16(숫자는 레이어 수를 가리킵니다, 기본적으로 VGG16와 VGG19 두 버전이 있음)를 처음부터 구현한 후, 우리의 데이터셋으로 학습시키고 테스트 세트에서 평가하여 알지 못한 데이터에서의 성능을 볼 것입니다


VGG

AlexNet의 작업에 기반하여 深さ를 주목하는 Convolutional Neural Networks (CNNs)의 또 다른 중요한 측면에 집중하는 VGG이 시 Monica Simonyan와 Andrew Zisserman이 발전시켰습니다. 一般的으로 16개의 卷積 层层로 구성되나, 19개의 层层로 확장할 수 있으며 (따라서 VGG-16과 VGG-19의 두 가지 버전이 있다) 모두 3×3 필터를 갖추고 있습니다. 공식 논문에서 네트워크에 대해 더 많은 정보를 읽으실 수 있습니다. 여기

VGG16 아키텍처에 대한 자세한 내용을 확인할 수 있습니다. 소스


데이터 로딩

데이터셋

대부분의 머신러닝 프로젝트에서 가장 중요한 것之一는 모델을 빌드하기 전에 데이터셋을 불러오고, 분석하며 사전 처리하는 것입니다. 이篇文章에서 우리는 CIFAR-100 데이터셋을 사용하게 됩니다. 이 데이터셋은 CIFAR-10와 비슷하지만, 600개의 이미지를 갖는 100개의 클래스가 있습니다. 각 클래스당 500개의 훈련 이미지와 100개의 테스트 이미지가 있습니다. CIFAR-100의 100개 클래스는 20개의 초 클래스로 그룹화되어 있습니다. 각 이미지는 “fine” 레이블(이미지가属하는 클래스)와 “coarse” 레이블(이미지가属하는 초 클래스)로 구성되어 있습니다. 우리는 여기서 “fine” 레이블을 사용하게 될 것입니다. CIFAR-100의 클래스 목록은 다음과 같습니다:


CIFAR-100 데이터셋의 클래스 목록

라이브러리 가져오기

우리는 주로 torch (모델 구축 및 훈련에 사용), torchvision (데이터 불러오기/처리, 컴퓨터 비전 데이터셋과 그 데이터셋 처리 방법을 포함), numpy (수학적 조작)를 사용하게 될 것입니다. 또한 device 변수를 정의하여 프로그램이 가능하다면 GPU를 사용할 수 있도록 하겠습니다.

import numpy as np
import torch
import torch.nn as nn
from torchvision import datasets
from torchvision import transforms
from torch.utils.data.sampler import SubsetRandomSampler


# 장치 구성
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

데이터 로딩

torchvision은 컴퓨터 비전 데이터셋 많은 것을 쉽게 접근하는 라이브러리이며, 이들 데이터셋을 간단하고 直观적으로 전처리하는 방법을 제공합니다.

  • 우리는 인자에 따라 훈련/검증 데이터나 시험 데이터를 돌려주는 data_loader 함수를 정의합니다.
  • 우리는 dataset의 각 채널(빨강, 녹색, 蓝色字体)의 평균과 표준 편차를 담는 변수 normalize를 정의합니다. 이것은 수동으로 계산할 수 있으며, 网上에도 사용 가능합니다. 이것은 데이터를 이진화하고, 툴팁으로 변환하고 그 后来에 일정화하는 transform 변수에 사용됩니다.
  • test 인자가 참이면, 데이터 로더를 사용하여 시험 분할을 넣고 돌려줍니다.(아래에서 설명합니다)
  • test 인자가 거짓이면(기본적인 behavior도 마찬가지), 훈련 분할을 넣고 무작위하게 훈련 및 검증 세트(0.9:0.1)로 나누어줍니다.
  • 결국, 데이터 로더를 사용합니다. CIFAR100과 같은 작은 dataset에 대해서는 성능에 영향을 미치지 않지만, 大型 dataset에 대해서는 실제로 성능을 obstruction할 수 있으며, 일반적으로 좋은 惯行이 되어 있습니다. 데이터 로더는 batch로 데이터를 이룰 수 있는 것을 허용합니다. 이를 통해 시작할 때 Ram에 모두 들어가지 않고, 이 iterating하는 것과 동시에 로드합니다.
def data_loader(data_dir,
                batch_size,
                random_seed=42,
                valid_size=0.1,
                shuffle=True,
                test=False):
  
    normalize = transforms.Normalize(
        mean=[0.4914, 0.4822, 0.4465],
        std=[0.2023, 0.1994, 0.2010],
    )

    # 변형 정의
    transform = transforms.Compose([
            transforms.Resize((227,227)),
            transforms.ToTensor(),
            normalize,
    ])

    if test:
        dataset = datasets.CIFAR100(
          root=data_dir, train=False,
          download=True, transform=transform,
        )

        data_loader = torch.utils.data.DataLoader(
            dataset, batch_size=batch_size, shuffle=shuffle
        )

        return data_loader

    # 데이터셋 로딩
    train_dataset = datasets.CIFAR100(
        root=data_dir, train=True,
        download=True, transform=transform,
    )

    valid_dataset = datasets.CIFAR10(
        root=data_dir, train=True,
        download=True, transform=transform,
    )

    num_train = len(train_dataset)
    indices = list(range(num_train))
    split = int(np.floor(valid_size * num_train))

    if shuffle:
        np.random.seed(random_seed)
        np.random.shuffle(indices)

    train_idx, valid_idx = indices[split:], indices[:split]
    train_sampler = SubsetRandomSampler(train_idx)
    valid_sampler = SubsetRandomSampler(valid_idx)

    train_loader = torch.utils.data.DataLoader(
        train_dataset, batch_size=batch_size, sampler=train_sampler)
 
    valid_loader = torch.utils.data.DataLoader(
        valid_dataset, batch_size=batch_size, sampler=valid_sampler)

    return (train_loader, valid_loader)


# CIFAR100 데이터셋
train_loader, valid_loader = data_loader(data_dir='./data',
                                         batch_size=64)

test_loader = data_loader(data_dir='./data',
                              batch_size=64,
                              test=True)

VGG16 scratch 로 만들기

Scratch로 모델을 만들기 위해서는 우선 torch 中 model definitions의 작동方式을 이해해야 합니다. 이를 위해 사용할 Layers의 종류를 이해하는 것이 중요합니다.:

  • Custom model은 nn.Module クラス를 상속받아야만 기본적인 機能을 가지고 있도록 모델을 训练 할 수 있습니다.
  • 두 가지 주요한 것을 해야 합니다. 첫째, __init__ 함수 안에 모델의 다양한 Layer를 정의하고, 이들 Layer가 입력에 대해 실행되는 순서를 forward 함수 안에 정의합니다.

이제 여기에서 사용하는 다양한 Layer 종류를 정의하겠습니다:

  • nn.Conv2d: 이것은 입력과 출력 チャンネル의 수를 인자로 받고, 필터의 크기를 갖는 卷積 层层입니다. 이것이 必要하면 스트라이드나 패딩을 받을 수 있습니다.
  • nn.BatchNorm2d: 이것은 卷積 层层의 출력에 batch normalization을 적용합니다.
  • nn.ReLU: 이것은 네트워크 내에서 다양한 출력에 적용되는 활성화 함수입니다.
  • nn.MaxPool2d : 지정한 kernel size로 output에 max pooling을 적용합니다.
  • nn.Dropout: 주어진 확률로 output에 dropout을 적용합니다.
  • nn.Linear: 기본적으로 전чно 연결된 层次입니다.
  • nn.Sequential: 层次의 유형이 아닌 것이지만, 동일한 단계의 여러 操作을 결합하는 것에 도움이 됩니다.

이러한 지식을 사용하여, 우리는 지문에 나오는 구조를 사용하여 VGG16 모델을 만들 수 있습니다.:

class VGG16(nn.Module):
    def __init__(self, num_classes=10):
        super(VGG16, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU())
        self.layer2 = nn.Sequential(
            nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(), 
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer3 = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU())
        self.layer4 = nn.Sequential(
            nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer5 = nn.Sequential(
            nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU())
        self.layer6 = nn.Sequential(
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU())
        self.layer7 = nn.Sequential(
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer8 = nn.Sequential(
            nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer9 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer10 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer11 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer12 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer13 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.fc = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(7*7*512, 4096),
            nn.ReLU())
        self.fc1 = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU())
        self.fc2= nn.Sequential(
            nn.Linear(4096, num_classes))
        
    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = self.layer5(out)
        out = self.layer6(out)
        out = self.layer7(out)
        out = self.layer8(out)
        out = self.layer9(out)
        out = self.layer10(out)
        out = self.layer11(out)
        out = self.layer12(out)
        out = self.layer13(out)
        out = out.reshape(out.size(0), -1)
        out = self.fc(out)
        out = self.fc1(out)
        out = self.fc2(out)
        return out

VGG16 from Scratch


Hyperparameters

모든 기계 或者 깊은 leaning 프로젝트의 중요한 부분은 hyper-parameters를 최적화하는 것입니다. 여기서는 이러한 값을 다양하게 시험하지 않고 이전에 정의해야 합니다. 이에 epoch의 수, 배치 사이즈, 학습률, 손실 関数 및 오timizer를 정의하는 것이 포함됩니다.

num_classes = 100
num_epochs = 20
batch_size = 16
learning_rate = 0.005

model = VGG16(num_classes).to(device)


# Loss and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay = 0.005, momentum = 0.9)  


# Train the model
total_step = len(train_loader)

hyper-parameters를 설정합니다.


Training

우리는 이제 모델을 훈련시키기 시작합니다. 우선 torch에서 모델을 어떻게 훈련시키는지 살펴보고 코드를 확인하겠습니다:

  • 각 epoch마다 우리는 train_loader 내의 이미지와 레이블을 순회하며, 가능한 경우 GPU로 이들을 옮깁니다. 이 과정은 자동으로 이루어집니다
  • 우리는 모델을 사용하여 레이블을 예측하고(model(images)) 예측 결과와 실제 레이블간의 손실을 손실 함수(criterion(outputs, labels))를 사용하여 계산합니다
  • 그리고 해당 손실을 사용하여 역전파(loss.backward)를 수행하고 가중치를 업데이트합니다(optimizer.step()). 하지만 각 업데이트 전에 기울기를 제로로 설정해야 합니다. 이는 optimizer.zero_grad()를 사용하여 수행됩니다
  • 또한 각 epoch의 끝에 우리는 검증 세트를 사용하여 모델의 정확도를 계산합니다. 이 경우에는 기울기가 필요 없으므로 with torch.no_grad()를 사용하여 더 빠른 평가를 수행합니다

이제 이 모든 것을 다음 코드에 통합합니다:

total_step = len(train_loader)

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        # 텐서를 설정된 장치로 이동
        images = images.to(device)
        labels = labels.to(device)
        
        # 전단 처리
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        # 역전파와 최적화
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, loss.item()))
            
    # 검증
    with torch.no_grad():
        correct = 0
        total = 0
        for images, labels in valid_loader:
            images = images.to(device)
            labels = labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
            del images, labels, outputs
    
        print('Accuracy of the network on the {} validation images: {} %'.format(5000, 100 * correct / total)) 

훈련

위 코드의 출력은 다음과 같이, 모델이 실제로 학습하고 있음을 보여주며, 각 epoch마다 손실이 줄어들고 있음을 보입니다:


훈련 손실


테스트

테스트 목적으로는 Validation과 동일한 코드를 test_loader를 사용하여 실행합니다.:

with torch.no_grad():
    correct = 0
    total = 0
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        del images, labels, outputs

    print('Accuracy of the network on the {} test images: {} %'.format(10000, 100 * correct / total))   

테스트

위의 코드를 사용하고 모델을 20 epochs 동안 训练 한 후, Teset 셋에 대한 정확도가 75%를 달성했습니다.


결론

이제 文章에서 실제로 하는 것에 대해 결론을 드립니다:

  • 우선 VGG-16 모델의 아키텍처와 다양한 层次을 이해하였습니다.
  • 次に, torchvision를 사용하여 CIFAR100 데이터셋을 로드하고 전처리하였습니다.
  • 그 다음, PyTorch를 사용하여 기반이 되는 VGG-16 모델을 만들고 torch에 있는 다양한 层次을 이해하였습니다.
  • 마지막으로, CIFAR100 데이터셋을 통해 모델을 训练하고 테스트하였고, 모델은 75%의 정확도로 테스트 데이터셋에 대해 좋게 동작하였습니다.

将来の작업

이 文章을 사용하면 좋은 소개와 现场 학습을 얻을 수 있지만, 이를 확장하고 다른 것을 할 수 있는지 여부에 대해 더 많은 것을 배울 수 있습니다.

  • 다른 데이터셋을 시도할 수 있습니다. 하나의 데이터셋은 CIFAR10이나 ImageNet 데이터셋의 미분 일부입니다.
  • 다양한 하이퍼パarameter를 시도하여 모델에 가장 좋은 조합을 확인할 수 있습니다.
  • 결국, 이 데이터셋에 层次를 추가하거나 제거하여 그들이 모델의 능력에 미칠 영향을 확인할 수 있습니다. 더 나은 것은, 이 모델의 VGG-19 버전을 빌드하는 것입니다.

Source:
https://www.digitalocean.com/community/tutorials/vgg-from-scratch-pytorch