PyTorchでAlexNetを从头から書く

紹介

この投稿は、PyTorchで最も人気のある畳み込み神经网路を从零開始構築するシリーズの一環です。前回の投稿はここにあり、私たちはLeNet5を構築しました。この投稿で、计算机ビジョンで最も重要な飛躍的な算法の1つであるAlexNetを構築します。

ここでは、AlexNetのアーキテクチャを調査し、理解します。その後、コードに突入し、私たちのデータセット、CIFAR-10を読み込んで、データに前処理を適用します。その後、PyTorchを使用してAlexNetを从零開始构築し、前処理されたデータに対してトレーニングします。最後に、学習したモデルを未見の(テスト)データに対して評価目的に試験します。

前提条件

ニューラルネットワークの知識がこの記事を理解するのに役立つでしょう。これには、ニューラルネットワークの異なる層(入力層、隐藏層、出力層)や激活関数、最適化アルゴリズム(勾配下降の異なるバリエーション)、損失関数などを熟悉していることが含まれます。また、この記事で取り上げられるコードスニペットを理解するためには、PythonのスyntaxとPyTorchライブラリの熟悉が必要です。

CNN(畳み込み Neural Network)の理解は欠かせません。これには、畳み込み層、プーリング層、および、入力データから特徴量を抽出する役割などを含む。ストライド、パディング、そしてカーネル/フィルターサイズの影響などの概念も理解することが有益です。

AlexNet

AlexNetは、2012年にAlex Krizhevskyと其の同僚によって最初に開発された深層畳み込み Neural Networkです。それは、ImageNet LSVRC-2010の画像分類コンペに対応し、最も優れた結果を得ました。模型的な研究論文の詳細

は、下記より読むことができます。まず、AlexNetは(224x224x3)の大きさの3チャンネルの画像を操作しました。最大プーリングを使用し、ReLU激活を使用した際には、畳み込みに使用されるカーネルは11×11、5×5、または3×3で、最大プーリングに使用されるカーネルは3×3の大きさでした。それは1000のクラスに画像を分類しました。また、複数のGPUを使用しました。

データセット

CIFAR-10データセットを使用して、データの読み込みと前処理を始めましょう。データセットには、10のクラスにつき6000枚の32×32の彩色画像が含まれ、クラスには6000枚の画像があります。データセットには50000のトレーニング画像と10000のテスト画像があります。

以下は、データセット内のクラスと各クラスの10个の乱数選択されたサンプル画像です。

元の出处: source

クラスは完全に排他的です。自動車とトラックは互いに重叠していません。”Automobile”にはsedan、SUVなどが含まれます。”Truck”には大型トラックのみが含まれます。ピックアップトラックは含まれません。

ライブラリの導入

まず、必要なライブラリを導入し、変数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(コンピュータビジョンのためのアシスタントライブラリ)を使用して、私たちのデータセットを読み込みます。この方法は、前処理を簡単にし、直線的にするためのいくつかのヘルパー関数を持っています。get_train_valid_loaderget_test_loader関数を定義し、そしてそれらを呼び出して、CIFAR-10データを読み込みます。

def get_train_valid_loader(data_dir,
                           batch_size,
                           augment,
                           random_seed,
                           valid_size=0.1,
                           shuffle=True):
    normalize = transforms.Normalize(
        mean=[0.4914, 0.4822, 0.4465],
        std=[0.2023, 0.1994, 0.2010],
    )

    # 変換を定義する
    valid_transform = transforms.Compose([
            transforms.Resize((227,227)),
            transforms.ToTensor(),
            normalize,
    ])
    if augment:
        train_transform = transforms.Compose([
            transforms.RandomCrop(32, padding=4),
            transforms.RandomHorizontalFlip(),
            transforms.ToTensor(),
            normalize,
        ])
    else:
        train_transform = transforms.Compose([
            transforms.Resize((227,227)),
            transforms.ToTensor(),
            normalize,
        ])

    # データセットを読み込む
    train_dataset = datasets.CIFAR10(
        root=data_dir, train=True,
        download=True, transform=train_transform,
    )

    valid_dataset = datasets.CIFAR10(
        root=data_dir, train=True,
        download=True, transform=valid_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)


def get_test_loader(data_dir,
                    batch_size,
                    shuffle=True):
    normalize = transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225],
    )

    # 変換を定義する
    transform = transforms.Compose([
        transforms.Resize((227,227)),
        transforms.ToTensor(),
        normalize,
    ])

    dataset = datasets.CIFAR10(
        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

# CIFAR10 データセット 
train_loader, valid_loader = get_train_valid_loader(data_dir = './data',                                      batch_size = 64,
                       augment = False,                                          random_seed = 1)

test_loader = get_test_loader(data_dir = './data',
                              batch_size = 64)

コードを分割しましょう:

  • 私たちは、それぞれ学習/検証セットとテストセットを読み込むget_train_valid_loaderget_test_loaderを定義します。
  • まず、normalize変数を定義し、データセットの各チャンネル(赤、緑、青)の平均と標準偏差を含みます。これらは手動で計算することもできますが、CIFAR-10は非常に人気があるため、オンラインでも利用できます。
  • 私たちの学習データセットには、学習をより強固にし、画像の数を増やすためのデータ増強のオプションも追加します。注意:増強は学習子セットにのみ適用され、検証およびテスト子セットには適用されません。彼らは評価のためだけに使用されます。
  • 学習データセットを、学習と検証セット(90:10の割合)に分割し、全体の学習セットからランダムなサブセットにする。
  • 読み込む際に、バッチサイズを指定し、データセットをシャッフルすることにします。これにより、各バッチにより多くのラベルの種類の变异があるようになります。これは、結果のモデルの効果を向上させます。
  • 最終的に、私たちはデータローダーを使用します。これはCIFAR-10のような小さなデータセットではパフォーマンスに影響を与えないかもしれませんが、大きなデータセットの場合は実際にパフォーマンスを低下させることがあり、一般的には良い慣習として考えられています。データローダーを使用することで、バッチの中でデータを迭代することができます。迭代中にデータを読み込み、RAMに一度に全てを読み込むのではなく

AlexNet from Scratch

まず、コードから始めましょう:

class AlexNet(nn.Module):
    def __init__(self, num_classes=10):
        super(AlexNet, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=0),
            nn.BatchNorm2d(96),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 3, stride = 2))
        self.layer2 = nn.Sequential(
            nn.Conv2d(96, 256, kernel_size=5, stride=1, padding=2),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 3, stride = 2))
        self.layer3 = nn.Sequential(
            nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(384),
            nn.ReLU())
        self.layer4 = nn.Sequential(
            nn.Conv2d(384, 384, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(384),
            nn.ReLU())
        self.layer5 = nn.Sequential(
            nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 3, stride = 2))
        self.fc = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(9216, 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 = out.reshape(out.size(0), -1)
        out = self.fc(out)
        out = self.fc1(out)
        out = self.fc2(out)
        return out

AlexNetモデルの定義

上記のコードがどのように機能するかを詳細に見ていきましょう:

  • PyTorchで何から neural network (CNNであればありますが、CNNでなくても)を定義するための最初のステップは、nn.Moduleを継承するクラスを定義することです。なぜなら、これには私たちが使用する必要のある多くのメソッドが含まれています
  • その後、2つの主要なステップがあります。まずは__init__内部で使用することになっている層を初期化し、次に、それらの層が画像を処理する順序を定義します。これはforward関数の内部に定義されます。
  • アーキテクチャ自体については、まずnn.Conv2D関数を使用して適切な核の大きさと入力/出力チャンネルを持つ畳み込み層を定義します。また、nn.MaxPool2D関数を使用して最大プーリングを適用します。PyTorchの良い所は、畳み込み層、活性化関数、最大プーリングを1つの単一の層(個別に適用されますが、組織性の改善に役立ちます)に结合することができることです。これらをnn.Sequential関数を使用して結合します
  • 次に、線形(nn.Linear)、ドロップアウト(nn.Dropout)、ReLU活性化関数(nn.ReLU)を使用した完全に結合層を定義します。これらをnn.Sequential関数を使用して結合します
  • 最後に、最終的な出力層は10ニューロンで構成され、これは10のクラスの物体に対する最終的な予測です

超参数設定

学習する前に、損失関数と使用する最適化器などのいくつかの超参数を設定する必要があります。また、バッチサイズ、学習率、エポック数も設定する必要があります

num_classes = 10
num_epochs = 20
batch_size = 64
learning_rate = 0.005

model = AlexNet(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)

私たちは、簡単な超パラメーター(エポック数、バッチサイズ、学習率)を定義し始め、この場合は10のクラス数を引数として、モデルを初期化し、適切なデバイス(CPUまたはGPU)にモデルを移行します。そして、交叉エントロピー損失をコスト関数、Adamを最適化器として定義します。これらには多くの選択がありますが、これらは一般的に、モデルと与えられたデータによって良い結果を提供すると倾向しています。最後に、total_stepを定義し、学習中のステップをより良く追跡することができます。

学習

この時点で、私たちはモデルを訓練する準備が整いました。

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()))

    # 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)) 

コードが何を行いますか?

  • まず、エポック数について回り、そして、訓練データのバッチを回ります。
  • 画像とラベルを、使用しているデバイスに基づいて変換します。つまり、GPUまたはCPUです。
  • 前方伝播で、私たちのモデルを使用して予測を行い、予測と実際のラベルに基づいて損失を計算します。
  • 次に、後方伝播を行い、重みを更新してモデルを改善することができます。
  • そして、更新の前に重みをゼロに設定するために、optimizer.zero_grad()関数を使用します。
  • それから、loss.backward()関数を使用して新しい勾配を計算します。
  • 最終的に、私たちは重みを `optimizer.step()` 関数で更新します
  • また、每一エポックの終わりに、私たちは validation setを使用してモデルの精度を計算します。この場合、gradientが必要ありませんので、より速い評価を実行するために `with torch.no_grad()` を使用します

以下のような出力を見ることができます

学習損失とvalidation精度

私たちは、損失が每一エポックごとに低下していることがわかりますので、実際にはモデルは学習しています。注意事項は、この損失は学習集中的なデータセットに適用されており、過学習を示すと思われる非常に小さな損失がある可能性があります。これがなぜかというと、私たちはvalidation setを使用しているからです。validation setでの精度は上昇していることがわかりますので、過学習がないことを示しています。今から、私たちのモデルの性能をテストしてみましょう。

テスト

今回、私たちは未見たデータに対してモデルの性能を見ます

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))   

このコードは、私たちのvalidation目的に使用したものと完全に同じです

6エポックでのトレーニング後、validation set上で約78.8%の精度を得ることができます

テスト精度

結論

この記事で私たちが行ったことをまとめてみましょう。

  • 私たちはまずAlexNetモデルのアーキテクチャと異なる種類の層を理解しました
  • 次に、torchvisionを使用してCIFAR-10データセットを読み取り、前処理しました
  • そして、PyTorchを使用してアルフレックスネットモデルを从头から構築しました
  • 最後に、CIFAR-10データセットでモデルを訓練およびテストし、最小限の訓練(6エポック)でテストデータセットで良いパフォーマンスを示しているようです

未来のワーク

この記事は坚实的基础と実践的な経験を提供しますが、さらに多くの知識を得るためには、さらに探索し、さらに何を成し遂げることができるか試してみてください。

  • 異なるデータセットを試してみてください。その1つはCIFAR-100で、CIFAR-10データセットを100のクラスの延長として提供しています
  • 異なる超参数を試して、模型的に最適な組み合わせを見つけてみてください
  • 最後に、データセットから層を追加または削除して、それらがモデルの能力に及ぼす影響を見てみてください。

Source:
https://www.digitalocean.com/community/tutorials/alexnet-pytorch