ウェブサイト検索

PyTorchでResNetをゼロから書く


この記事では、コンピュータビジョンにおける重要なブレークスルーであるResNetを構築します。このネットワークは、ネットワークが深すぎるとパフォーマンスが低下するという問題を解決しました。また、残差接続の概念も導入されました(このことについては後で詳しく説明します)。

私たちは、ResNetのアーキテクチャとその動作原理を考察することから始めます。そして、VGGと比較し、VGGが抱えていたいくつかの問題をどのように解決するのかを検証します。それから、前回と同様に、データセットであるCIFAR10を読み込み、モデリングの準備をするために前処理を行います。その後、まずResNetの基本的なビルディングブロック(これをResidualBlockと呼びます)を実装し、それを使ってネットワークを構築します。その後、このネットワークは前処理されたデータで訓練され、最後に訓練されたモデルが見たことのないデータ(テストセット)でどのようにパフォーマンスを発揮するかを確認します。

前提条件

この記事を理解するためには、Pythonコードの基本的な経験とディープラーニングの初歩的な理解が必要です。すべての読者が提供されたコードを実行できるだけの十分な性能を持つマシンにアクセスできると仮定します。性能が低いGPUも使用できますが、結果が得られるまでに時間がかかる可能性があります。

GPUにアクセスできない場合は、クラウドを介してアクセスすることをお勧めします。GPUを提供するクラウドプロバイダーは多数あります。DigitalOceanのGPUドロップレットは現在、早期提供中です。こちらで詳細を確認し、GPUドロップレットへの興味を登録してください。

Pythonコードの始め方についての指示には、システムを設定し初心者向けのチュートリアルを実行する準備をするための初心者向けガイドを試すことをお勧めします。

ResNet

VGGの欠点の一つは、過剰適合し始めるため、望むほど深くすることができなかったことです。これは、ニューラルネットワークが深くなるにつれて、損失関数からの勾配がゼロに縮小し始めるためであり、その結果、重みが更新されなくなるためです。この問題は消失勾配問題として知られています。ResNetは実際にスキップ接続を使用することにより、この問題を解決しました。

残差ブロック。出典:ResNet論文

上の図では、通常の接続に加えて、モデル内のいくつかの層を飛ばす直接接続(スキップ接続)があることがわかります。スキップ接続を使うことで、出力はh(x)=f(wx +b)からh(x)=f(x) + x.に変更されます。これらのスキップ接続は、勾配が流れるための代替のショートカットパスを可能にするため、役立ちます。以下は、34層のResNetのアーキテクチャです。

ソース: ResNet 論文

データロード:データセット

この記事では、有名なCIFAR-10データセットを使用します。これは初心者のコンピュータビジョンデータセットとして最も一般的な選択肢の1つになっています。このデータセットは、8000万枚の小さな画像データセットのラベル付きサブセットです。これらの画像は、アレックス・クリーゼフスキー、ヴィノード・ネア、ジェフリー・ヒントンによって収集されました。CIFAR-10データセットは、10クラスの合計60,000枚の32x32カラー画像で構成されており、クラスごとに6,000枚の画像があります。訓練用画像は50,000枚、テスト用画像は10,000枚です。

データセットは5つのトレーニングバッチと1つのテストバッチに分かれており、各バッチには10,000枚の画像があります。テストバッチには各クラスからランダムに選ばれた1,000枚の画像が正確に含まれています。トレーニングバッチには、残りの画像がランダムな順序で含まれていますが、一部のトレーニングバッチには他のクラスよりも多くの画像が含まれる場合があります。それらの間で、トレーニングバッチには各クラスから正確に5,000枚の画像が含まれています。クラスは完全に相互排他的です。自動車とトラックの間には重複はありません。「自動車」にはセダン、SUV、その他そのようなものが含まれます。「トラック」には大型トラックのみが含まれます。いずれにもピックアップトラックは含まれていません。

データセットのクラスと、それぞれからの10枚のランダムな画像は次のとおりです:

ソース:ペーパー

ライブラリをインポートする

使用するライブラリをインポートすることから始めます。それに加えて、ノートブックがモデルをトレーニングするために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 configuration
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

ライブラリをインポートする

データセットをロードしています

次に、データセットの読み込みに移ります。この目的のために、私たちはtorchvisionライブラリを使用します。このライブラリは、数百のコンピュータビジョン用データセットへの迅速なアクセスを提供するだけでなく、モデリングの準備が整うようにそれらを前処理/変換するための簡単で直感的な方法も提供します。

  • 最初に、引数に応じてトレーニングデータまたはテストデータを返す data_loader 関数を定義します。

  • 深層学習プロジェクトでは、データを正規化することは常に良い習慣です。これにより、トレーニングが迅速になり、収束しやすくなります。このため、データセット内の各チャンネル(赤、緑、青)の平均と標準偏差を持つ変数normalizeを定義します。これらは手動で計算できますが、オンラインでも入手可能です。この変数は、データをリサイズし、テンソルに変換してから正規化するtransform変数で使用されます。

  • 私たちはデータローダーを利用します。データローダーを使用すると、データをバッチで反復処理することができ、データは反復処理中に読み込まれ、一度にすべてをRAMに読み込むのではありません。これは、約100万枚の画像を含む大規模なデータセットを扱う場合に非常に役立ちます。

  • 引数 test に応じて、トレイン(test=False の場合)スプリットを読み込むか、テスト(test=True の場合)スプリットを読み込みます。トレインの場合、スプリットはランダムにトレインセットとバリデーションセット(0.9:0.1)に分割されます。

    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((224,224)),
              transforms.ToTensor(),
              normalize,
      ])
    
      if test:
          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
    
      # load the dataset
      train_dataset = datasets.CIFAR10(
          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(42)
          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)
    train_loader, valid_loader = data_loader(data_dir='./data',
                                           batch_size=64)
    
    test_loader = data_loader(data_dir='./data',
                                batch_size=64,
                                test=True)
    

スクラッチからのResNet:PyTorchにおけるモデルの仕組み

残差ブロックとResNetの構築に移る前に、まずPyTorchで神経ネットワークがどのように定義されているかを見て理解しましょう:

  • nn.Moduleはカスタムモデルを作成するためのひな形を提供し、トレーニングに役立ついくつかの必要な機能を備えています。そのため、すべてのカスタムモデルはnn.Moduleから継承する傾向があります。
  • 次に、カスタムモデル内には2つの主な機能があります。最初は初期化関数__init__で、使用するさまざまなレイヤーを定義します。2つ目はforward関数で、上記のレイヤーが特定の入力に対してどの順序で実行されるかを定義します。

PyTorchのレイヤー

次に、私たちに役立つPyTorchで利用可能な異なる種類のレイヤーについて説明します:

  • nn.Conv2d: これは、フィルタのカーネルサイズと共に、入力および出力チャネルの数を引数として受け入れる畳み込み層です。また、適用したい場合は、ストライドやパディングも受け入れます。
  • nn.BatchNorm2d: これは畳み込み層の出力にバッチ正規化を適用します
  • nn.ReLU: これはネットワーク内のさまざまな出力に適用される一種の活性化関数です。
  • nn.MaxPool2d : これは、指定されたカーネルサイズを用いて出力に最大プーリングを適用します。
  • nn.Dropout: これは与えられた確率で出力にドロップアウトを適用するために使用されます
  • nn.Linear: これは基本的に全結合層です。
  • nn.Sequential: これは技術的にはレイヤーの型ではありませんが、同じステップに属するさまざまな操作を組み合わせるのに役立ちます。

残差ブロック

ネットワークを始める前に、ネットワーク全体で再利用できるResidualBlockを構築する必要があります。このブロック(アーキテクチャに示されているように)には、オプションのパラメータ(downsample)であるスキップ接続が含まれています。forward内で、これは出力outではなく、入力xに直接適用されることに注意してください。

class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride = 1, downsample = None):
        super(ResidualBlock, self).__init__()
        self.conv1 = nn.Sequential(
                        nn.Conv2d(in_channels, out_channels, kernel_size = 3, stride = stride, padding = 1),
                        nn.BatchNorm2d(out_channels),
                        nn.ReLU())
        self.conv2 = nn.Sequential(
                        nn.Conv2d(out_channels, out_channels, kernel_size = 3, stride = 1, padding = 1),
                        nn.BatchNorm2d(out_channels))
        self.downsample = downsample
        self.relu = nn.ReLU()
        self.out_channels = out_channels

    def forward(self, x):
        residual = x
        out = self.conv1(x)
        out = self.conv2(out)
        if self.downsample:
            residual = self.downsample(x)
        out += residual
        out = self.relu(out)
        return out

ResidualBlock

ResNet34

これでResidualBlockを作成したので、ResNetを構築できます。

アーキテクチャには、それぞれ3層、3層、6層、そして3層の3つのブロックがあります。このブロックを作成するために、ヘルパー関数_make_layerを作成します。この関数は、レイヤーを一つずつ追加し、残差ブロックと組み合わせます。ブロックの後に、平均プーリングと最終的な線形層を追加します。

class ResNet(nn.Module):
    def __init__(self, block, layers, num_classes = 10):
        super(ResNet, self).__init__()
        self.inplanes = 64
        self.conv1 = nn.Sequential(
                        nn.Conv2d(3, 64, kernel_size = 7, stride = 2, padding = 3),
                        nn.BatchNorm2d(64),
                        nn.ReLU())
        self.maxpool = nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)
        self.layer0 = self._make_layer(block, 64, layers[0], stride = 1)
        self.layer1 = self._make_layer(block, 128, layers[1], stride = 2)
        self.layer2 = self._make_layer(block, 256, layers[2], stride = 2)
        self.layer3 = self._make_layer(block, 512, layers[3], stride = 2)
        self.avgpool = nn.AvgPool2d(7, stride=1)
        self.fc = nn.Linear(512, num_classes)

    def _make_layer(self, block, planes, blocks, stride=1):
        downsample = None
        if stride != 1 or self.inplanes != planes:

            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes, kernel_size=1, stride=stride),
                nn.BatchNorm2d(planes),
            )
        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample))
        self.inplanes = planes
        for i in range(1, blocks):
            layers.append(block(self.inplanes, planes))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.maxpool(x)
        x = self.layer0(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)

        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)

        return x

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

モデルのさまざまなハイパーパラメータに対して異なる値を試すことは常に推奨されていますが、ここでは1つの設定のみを使用します。それでも、皆さんが異なるものを試して、どれが最適かを見てみることをお勧めします。ハイパーパラメータには、エポック数、バッチサイズ、学習率、損失関数、最適化手法を定義することが含まれます。私たちはResNetの34層バリアントを構築しているので、適切な層数を渡す必要があります。

num_classes = 10
num_epochs = 20
batch_size = 16
learning_rate = 0.01

model = ResNet(ResidualBlock, [3, 4, 6, 3]).to(device)

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

#Train the model
total_step = len(train_loader)

トレーニング

さて、私たちのモデルはトレーニングの準備が整いましたが、まず最初にPyTorchでのモデルトレーニングがどのように機能するかを知る必要があります。

  • 私たちは、各エポックごとにtrain_loaderを使用して画像をバッチでロードし、前に定義したdevice変数を使用してデータをGPUに移動します。

  • モデルはその後、ラベルに対して予測を行い、model(images) を実行します。そして、上記で定義した損失関数を使用して、予測と真の値の間の損失を計算します。criterion(outputs, labels)

  • 今、学習の部分に入ります。損失をバックプロパゲートする方法を使用し、loss.backward() を実行して重みを更新します。重みを更新する前に必ず必要なことは、optimizer.zero_grad() を使用して勾配をゼロに設定することです。さもなければ、勾配が蓄積されてしまいます(PyTorchのデフォルトの動作)。

  • 最後に、各エポックの後に、バリデーションセットでモデルをテストしますが、評価時には勾配が必要ないため、with torch.no_grad()を使用して無効にし、評価をより速くすることができます。

    import gc
    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()
          del images, labels, outputs
          torch.cuda.empty_cache()
          gc.collect()
    
      print ('Epoch [{}/{}], Loss: {:.4f}' 
                     .format(epoch+1, num_epochs, 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)) 
    

コードの出力を分析すると、損失が減少し、検証セットの精度がエポックごとに増加しているため、モデルが学習していることが分かります。しかし、最後の方で振動していることに気付くかもしれません。これはモデルが過学習しているか、batch_size が小さいことを意味する可能性があります。何が起こっているのかを確認するためにテストを行う必要があります:

テスト

テストのために、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))   

上記のコードを使用してモデルを10エポックトレーニングした結果、テストセットで82.87%の精度を達成することができました:

精度のテスト

結論

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

  • 私たちはまず、アーキテクチャとResNetがどのように機能するかを理解することから始めました。
  • 次に、torchvisionを使用してCIFAR10データセットをロードし、前処理しました。
  • それから、私たちはPyTorchにおけるカスタムモデル定義の仕組みと、torchで利用可能なさまざまな種類のレイヤーについて学びました。
  • 私たちはResidualBlockを構築することによって、スクラッチからResNetを構築しました。
  • 最後に、私たちはCIFAR10データセットでモデルを訓練し、テストしました。そして、モデルはテストデータセットで75%の精度で良好にパフォーマンスを発揮したようです。

未来の仕事

この記事を使って、私たちは良い導入と実践的な学習を得ることができましたが、他の課題にこれを拡張すれば、もっと多くのことを学ぶことができます。

  • 異なるデータセットを使用してみてください。一例としてCIFAR100というImageNetデータセットのサブセットや、8000万枚の小さな画像データセットがあります。
  • 異なるハイパーパラメータを試し、それらの最適な組み合わせをモデルのために見つけてください。
  • 最後に、データセットから層を追加または削除して、モデルの能力に与える影響を見てみてください。さらに良いことに、このモデルのResNet-51バージョンを構築してみてください。

関連記事: