ウェブサイト検索

PyTorchでVGGをゼロから書く


コンピュータビジョンの分野において、過去1~2十年の間に革命をもたらした古典的な畳み込みニューラルネットワークを構築するシリーズを続けており、次はVGGという非常に深い畳み込みニューラルネットワークをPyTorchを使ってゼロから構築します。このシリーズの前回の記事(主にLeNet5とAlexNet)は私のプロフィールで見ることができます。

以前と同様に、VGGのアーキテクチャとその直感、当時の結果について検討します。その後、データセットであるCIFAR100を探求し、メモリ効率の良いコードを使用してプログラムに読み込みます。次に、PyTorchを使用してVGG16(番号は層の数を示し、基本的にVGG16とVGG19の2つのバージョンがあります)をゼロから実装し、データセットをトレーニングし、見たことのないデータに対する性能を確認するためにテストセットで評価します。

VGG

AlexNetの研究を基に、VGGは畳み込みニューラルネットワーク(CNN)のもう一つの重要な側面、深さに焦点を当てています。これはSimonyanとZissermanによって開発されました。通常は16層の畳み込み層で構成されますが、19層に拡張することも可能です(したがって、VGG-16とVGG-19の2つのバージョンがあります)。すべての畳み込み層は3x3のフィルターで構成されています。このネットワークについては、こちらの公式論文で詳しく読むことができます。

VGG16アーキテクチャ。出典

データの読み込み

データセット

モデルを構築する前に、すべての機械学習プロジェクトにおいて最も重要なことの一つは、データセットをロード、分析、前処理することです。本記事では、CIFAR-100データセットを使用します。このデータセットはCIFAR-10と似ていますが、100クラスがそれぞれ600画像を含んでいます。各クラスには500のトレーニング画像と100のテスト画像があります。CIFAR-100の100クラスは20のスーパークラスにグループ化されています。各画像には「ファイン」ラベル(その画像が属するクラス)と「コース」ラベル(その画像が属するスーパークラス)が付いています。ここでは「ファイン」ラベルを使用します。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 configuration
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

データを読み込み中

torchvisionは、多くのコンピュータビジョンデータセットへの簡単なアクセスと、これらのデータセットを簡単かつ直感的な方法で前処理するためのメソッドを提供するライブラリです

  • data_loaderという関数を定義し、引数に応じて訓練/検証データまたはテストデータを返します。
  • まず、データセット内の各チャネル(赤、緑、青)の平均と標準偏差で変数normalizeを定義します。これらは手動で計算できますが、オンラインでも入手可能です。この値はtransform変数で使用され、データのサイズを変更し、テンソルに変換し、その後正規化します。
  • もし test 引数が真であれば、データセットのテスト分割をロードし、下に説明するデータローダーを使ってそれを返します。
  • もしtestが偽である場合(デフォルトの動作として)、データセットのトレイン分割を読み込み、それをランダムにトレインセットとバリデーションセット(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],
    )

    # define transforms
    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

    # load the dataset
    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 dataset 
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

モデルをゼロから構築するためには、まずtorchにおけるモデル定義の仕組みと、ここで使用するさまざまなタイプのレイヤーを理解する必要があります。

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

ここで使用しているさまざまなタイプのレイヤーを定義しましょう:

  • nn.Conv2d: これは、フィルタのカーネルサイズと共に、入力および出力チャネルの数を引数として受け入れる畳み込み層です。また、適用したい場合は、任意のストライドやパディングも受け入れます。
  • nn.BatchNorm2d: これは畳み込み層の出力にバッチ正規化を適用します
  • 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

ハイパーパラメータ

機械学習や深層学習プロジェクトの重要な部分の一つは、ハイパーパラメータを最適化することです。ここでは、異なる値で実験するのではなく、あらかじめそれらを定義する必要があります。これにはエポック数、バッチサイズ、学習率、損失関数、そしてオプティマイザの定義が含まれます。

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)

ハイパーパラメータの設定

トレーニング

私たちは今、モデルの訓練を行う準備が整いました。まず、torchでモデルを訓練する方法を見てから、コードを見ていきます:

  • 各エポックごとに、私たちはtrain_loader内の画像とラベルを処理し、利用可能な場合はそれらの画像とラベルをGPUに移動します。これは自動的に行われます。
  • 私たちは、モデルを使用してラベルに対して予測を行い(model(images))、その後、予測と真のラベルとの間の損失を損失関数(criterion(outputs, labels))を使用して計算します。
  • 次に、その損失を使用して逆伝播を行い(loss.backward)、重みを更新します(optimizer.step())。ただし、毎回の更新の前に勾配をゼロに設定することを忘れないでください。これは、optimizer.zero_grad()を使用して行います。
  • また、各エポックの最後に、検証セットを使用してモデルの精度を計算します。この場合、勾配は必要ないため、より速い評価のために with torch.no_grad() を使用します。

さて、これらすべてを次のコードに統合します:

total_step = len(train_loader)

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        # Move tensors to the configured device
        images = images.to(device)
        labels = labels.to(device)
        
        # Forward pass
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        # Backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, loss.item()))
            
    # Validation
    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%の精度で良好にパフォーマンスを発揮したようです。

未来の仕事

この記事を使用することで、良い導入と実践的な学習が得られますが、これを拡張して他に何ができるかを見れば、もっと多くのことを学ぶことができます:

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

関連記事: