PyTorchで从 scratch に VGGを書く

私のシリーズを続けて、過去10-20年間で计算机ビジョン领域を革新した古典的な畳み込み神经ネットワークの構築について述べます。今回は、PyTorchを使用して原始的なVGGという非常に深い畳み込み神经ネットワークを構築します。このシリーズの前の記事は、私のプロフィールで見ることができ、主にLeNet5AlexNetです。

今回も以前と同様に、VGGのアーキテクチャとその直观性、そして当時の結果について探ることにします。次に、私たちのデータセット、CIFAR100を探ることにし、最適なメモリ使用法のコードを使用してプログラムに読み込みます。その後、PyTorchを使用して原始的にVGG16(数字は層の数を指し、基本的にはVGG16とVGG19の2つのバージョンがあります)を実装し、それを私たちのデータセットにトレーニングし、テストセットに評価して、未見のデータ上でのパフォーマンスを見ます。


VGG

AlexNetの成果に基づいて、VGGは畳み込み神経망(CNN)の別の重要な側面、深さに焦点を置く。それはSimonyanとZissermanによって開発されました。通常、16の畳み込み層で構成されますが、19層にも拡張できます(そのため、VGG-16とVGG-19の両方のバージョンがあります)。すべての畳み込み層は3×3のフィルターを含みます。官方論文ここ

から、ネットワークについて詳しく読むことができます。情報源


データ読み込み

データセット

モデルの構築前に、マシン学習プロジェクトで最も重要なのは、データセットを読み取り、分析し、前処理することです。この記事では、CIFAR-100データセットを使用します。このデータセットは、CIFAR-10と同じようなものですが、100クラスにあり、それぞれ600画像が含まれます。それぞれのクラスには、500の学習画像と100のテスト画像があります。CIFAR-100の100のクラスは、20の超クラスに分けられています。それぞれの画像には、「fine」ラベル(それが属しているクラス)、および「coarse」ラベル(それが属している超クラス)が付属しています。ここでは「fine」ラベルを使用します。CIFAR-100のクラス列表は以下の通りです。


CIFAR-100データセットのクラス一覧

ライブラリの導入

ここでは、torch(モデルの構築や学習に使用)、torchvision(データの読み取り/処理、コンピュータビジョン用のデータセットとその処理方法を含む)およびnumpy(数学的な操作)を主に使用します。また、GPUが利用可能であれば使用できるように変数deviceを定義します。

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は、コンピュータビジョンの大量的なデータセットに簡単なアクセスを提供し、これらのデータセットを簡単で直观的な方法で前処理する機能を提供しています

  • train/validationデータまたはtestデータを引数に応じて返すdata_loader関数を定義します
  • まず、normalize変数を定義します。これには、データセット内の各チャンネル(赤、緑、青)の平均と標準偏差が含まれます。これらは手動で計算することもできますが、オンラインでも入手できます。これは、データをリサイズし、テンソルに変換した後に正規化するtransform変数に使用されます
  • test引数が真の場合、データセットのtest分割を簡単に読み込み、以下で説明するデータローダーを使用して返します
  • test引数が假の場合(デフォルトの動作でもあります)、train分割のデータを読み込み、乱数でtrainとvalidationセット(0.9:0.1)に分割します
  • 最後に、データローダーを使用します。これはCIFAR100のような小さなデータセットでは性能に影響を与えないかもしれませんが、大きなデータセットでは性能を大幅に低下させることがあり、一般的に良い慣習とされています。データローダーは、バッチごとにデータに対してイテレーションすることができます。この時、イテレーション中にデータを読み込み、すべてのデータを一度にRAMに読み込むのではなくできます。
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 中でモデル定義がどのように機能しているかを理解する必要があり、ここで使用する異なる種類の層の仕組みを学ぶ必要があります。

  • すべてのカスタムモデルは nn.Module クラスを継承する必要があり、これはモデルのトレーニングを助ける基本的な機能を提供します。
  • 次に、私たちがする必要がある主要なことは二つあります。まず、__init__ 関数内で私のモデルの異なる層を定義し、これらの層が入力に対して実行される順序をforward 関数内で定義する必要があります。

今、私たちが使用している異なる種類の層を定義しましょう。

  • nn.Conv2d: これらはフィルターの kernel size を引数として受け取る convolutional layers です。入力と出力チャンネルの数、以及び strides や padding も必要であれば受け取ることができます。
  • nn.BatchNorm2d: これは convolutional layer の出力に batch normalization を適用します。
  • nn.ReLU: これは网络上でさまざまな出力に適用される激活関数です。
  • nn.MaxPool2d : これは与えられたカーネルサイズで最大プーリングを適用する
  • nn.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のScratchから


超参数

どのような機械学習や深層学習プロジェクトでも重要な部分があり、それは超 parameterを最適化することです。ここで、異なる値に実験するのではなく、事前に定義する必要があります。これには、エポックの数、ミスのサイズ、学習率、損失関数と最適化器が含まれます。

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

model = VGG16(num_classes).to(device)


# 損失と最適化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay = 0.005, momentum = 0.9)  


# モデルのトレーニング
total_step = len(train_loader)

超 parameterの設定


トレーニング

私たちは、現在、モデルのトレーニングを行う準備ができています。まず、torchでモデルをトレーニングする方法を見て、その後、コードを確認します。

  • すべてのエポックにつき、私たちのtrain_loader内の画像とラベルを通り、利用可能な場合はGPUにそれらを移動します。これは自動的に行われます。
  • 私たちは、モデルを使用してラベルに予測を行い、予測と真のラベル間の損失をloss functionを使用して計算します。(model(images))criterion(outputs, labels)を使用します。
  • そして、その損失を逆向き传播します。(loss.backward)重みを更新します。(optimizer.step())ただ、更新する前に勾配を0に設定してください。これはoptimizer.zero_grad()を使用して行われます。
  • また、すべてのエポックの最後に、私たちはvalidation setを使用してモデルの精度を計算します。この場合、勾配が必要없으므로、より速い評価を行うためにwith torch.no_grad()を使用します。

次に、これらを以下のコードに結合します。

total_step = len(train_loader)

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        #  tensorを設定されたデバイスに移動
        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)) 

トレーニング

上記のコードの出力を以下のように確認することができます。これは、モデルが実際に学習していることを示しています。なぜなら、每一次に損失が減少しているからです。


トレーニング損失


テスト

テストのためには、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エポックでモデルをトレーニングしました。その結果、テストセットの精度が75%に達しました。


結論

この記事で行ったことをまとめましょう:

  • まず、VGG-16モデルの構造と各种の層を理解しました。
  • 次に、torchvisionを使用してCIFAR100データセットを読み取り、前処理しました。
  • そして、PyTorchを使用して、torchにあるさまざまな種類の層を理解しながら、自己的VGG-16モデルを構築しました。
  • 最後に、CIFAR100データセットでモデルをトレーニングし、テストしました。そして、モデルはテストデータセットで75%の精度で良く機能していました。

将来的な研究

この記事を通じて、良いイントロダクションと実際の学習ができますが、さらに多くを学ぶためには、これを拡張し、他に何ができるか探るのがおすすめです。

  • datasetを異なるものを試してみてください。その一つはCIFAR10またはImageNetデータセットの一部です。
  • 異なるハイパーパラメーターを試して、モデルに最適な組み合わせを見つけることができます。
  • 最後に、データセットから層の追加または削除を試して、それらがモデルの能力に及ぼす影響を見ます。さらに良いことに、このモデルのVGG-19バージョンを構築してみてください。

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