ウェブサイト検索

PyTorchでLeNet5をゼロから書く


紹介

この記事では、最も初期の畳み込みニューラルネットワークの1つであるLeNet5を構築します。このCNNをPyTorchでゼロから構築し、実際のデータセットでのパフォーマンスも見ていきます。

私たちはLeNet5のアーキテクチャを探ることから始めます。次に、提供されたtorchvisionのクラスを使用して、データセットMNISTを読み込み、分析します。PyTorchを使用して、私たちのデータに基づいてLeNet5を最初から構築し、それを訓練します。最後に、モデルが見たことのないテストデータでどのようにパフォーマンスを発揮するかを見ていきます。

前提条件

ニューラルネットワークの知識は、この記事を理解するのに役立ちます。これは、ニューラルネットワークの異なる層(入力層、隠れ層、出力層)、活性化関数、最適化アルゴリズム(勾配降下法のバリエーション)、損失関数などに慣れていることを意味します。さらに、この記事で示されるコードスニペットを理解するためには、Pythonの構文やPyTorchライブラリに慣れていることが不可欠です。

CNN(畳み込みニューラルネットワーク)についての理解も推奨されます。これには、畳み込み層やプーリング層、そしてそれらが入力データから特徴を抽出する役割に関する知識が含まれます。ストライド、パディング、カーネル/フィルターサイズの影響などの概念を理解することも有益です。

レネット5

LeNet5は手書き文字の認識に使用され、1998年にYann LeCunらによって「文書認識に応用された勾配ベースの学習」という論文で提案されました。

下の図に示すように、LeNet5のアーキテクチャを理解しましょう:

名前が示すように、LeNet5は2つの畳み込み層と3つの全結合層を持つ5層です。まず、入力から始めましょう。LeNet5は32x32のグレースケール画像を入力として受け入れます。これは、アーキテクチャがRGB画像(複数のチャンネル)には適していないことを示しています。したがって、入力画像は1つのチャンネルのみを含む必要があります。その後、畳み込み層に進みます。

最初の畳み込み層は、6つのフィルターを持つ5x5のフィルターサイズです。これにより、画像の幅と高さが減少し、深さ(チャンネル数)が増加します。出力は28x28x6になります。その後、プーリングが適用され、特徴マップが半分に減少し、14x14x6になります。同じフィルターサイズ(5x5)で16のフィルターが出力に適用され、その後プーリング層が続きます。これにより、出力特徴マップは5x5x16に減少します。

この後、120のフィルターを持つ5x5の畳み込み層が適用され、特徴マップが120の値に平坦化されます。次に、最初の全結合層が登場し、84のニューロンがあります。最後に、出力層があり、10の出力ニューロンがあり、MNISTデータには10個の数値数字を表すための10のクラスがあります。

データの読み込み

データの読み込みと分析から始めましょう。私たちはMNISTデータセットを使用します。MNISTデータセットには手書きの数字の画像が含まれています。画像はグレースケールで、すべてのサイズは28x28で、60,000のトレーニング画像と10,000のテスト画像で構成されています。

以下にいくつかの画像サンプルを見ることができます:

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

必要なライブラリをインポートし、いくつかの変数(ハイパーパラメーターおよびdeviceは、パッケージがGPUまたはCPUで訓練するかどうかを判断するのに役立つために詳述します)を定義することから始めましょう。

Load in relevant libraries, and alias where appropriate
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms

Define relevant variables for the ML task
batch_size = 64
num_classes = 10
learning_rate = 0.001
num_epochs = 10

Device will determine whether to run the training on GPU or CPU.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

データの読み込みと変換

torchvisionを使ってデータセットを読み込みます。これにより、前処理ステップを簡単に実行できるようになります。

#Loading the dataset and preprocessing
train_dataset = torchvision.datasets.MNIST(root = './data',
                                           train = True,
                                           transform = transforms.Compose([
                                                  transforms.Resize((32,32)),
                                                  transforms.ToTensor(),
                                                  transforms.Normalize(mean = (0.1307,), std = (0.3081,))]),
                                           download = True)


test_dataset = torchvision.datasets.MNIST(root = './data',
                                          train = False,
                                          transform = transforms.Compose([
                                                  transforms.Resize((32,32)),
                                                  transforms.ToTensor(),
                                                  transforms.Normalize(mean = (0.1325,), std = (0.3105,))]),
                                          download=True)


train_loader = torch.utils.data.DataLoader(dataset = train_dataset,
                                           batch_size = batch_size,
                                           shuffle = True)


test_loader = torch.utils.data.DataLoader(dataset = test_dataset,
                                           batch_size = batch_size,
                                           shuffle = True)

コードを理解しましょう:

  • まず最初に、MNISTデータはLeNet5アーキテクチャにはそのまま使用できません。LeNet5アーキテクチャは32x32の入力を受け入れますが、MNIST画像は28x28です。これを修正するためには、画像のサイズを変更し、事前に計算された平均と標準偏差(オンラインで入手可能)を使用して正規化し、最終的にテンソルとして保存します。
  • データがまだダウンロードされていない場合に備えて、download=Trueを設定します。
  • 次に、データローダーを使用します。MNISTのような小規模なデータセットの場合、パフォーマンスには影響しないかもしれませんが、大規模なデータセットの場合にはパフォーマンスを著しく妨げることがあり、一般的に良い習慣と考えられています。データローダーを使用すると、データをバッチ単位で反復処理することができ、データは反復処理中にロードされ、一度に全てをロードするのではありません。
  • バッチサイズを指定し、データセットをロードする際にシャッフルすることで、各バッチが含むラベルの種類にバリエーションが生まれます。これにより、最終的なモデルの効率が向上します。

スクラッチからのLeNet5

まずはコードを見てみましょう:

#Defining the convolutional neural network
class LeNet5(nn.Module):
    def __init__(self, num_classes):
        super(ConvNeuralNet, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=0),
            nn.BatchNorm2d(6),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer2 = nn.Sequential(
            nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0),
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.fc = nn.Linear(400, 120)
        self.relu = nn.ReLU()
        self.fc1 = nn.Linear(120, 84)
        self.relu1 = nn.ReLU()
        self.fc2 = nn.Linear(84, num_classes)
        
    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = out.reshape(out.size(0), -1)
        out = self.fc(out)
        out = self.relu(out)
        out = self.fc1(out)
        out = self.relu1(out)
        out = self.fc2(out)
        return out

LeNet5モデルの定義

コードを線形に説明します:

  • PyTorchでは、必要な多くのメソッドを含むnn.Moduleから継承したクラスを作成することでニューラルネットワークを定義します。
  • その後、主に2つのステップがあります。最初は、__init__内でCNNで使用するレイヤーを初期化することと、もう1つはそれらのレイヤーが画像を処理する順序を定義することです。これはforward関数内で定義されます。
  • アーキテクチャ自体については、まず適切なカーネルサイズと入力/出力チャネルを指定して、nn.Conv2D関数を使用して畳み込み層を定義します。また、nn.MaxPool2D関数を使用して最大プーリングも適用します。PyTorchの良い点は、畳み込み層、活性化関数、および最大プーリングを一つのレイヤーに統合できることです(それぞれは別々に適用されますが、組織化に役立ちます) nn.Sequential関数を使用することで。
  • 次に、全結合層を定義します。ここで nn.Sequential を使用して、活性化関数と線形層を組み合わせることもできますが、どちらでも可能であることを示したいと思いました。
  • 最後に、私たちの最終層は10個のニューロンを出力し、これは数字の最終的な予測です。

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

トレーニングの前に、使用する損失関数や最適化アルゴリズムなどのハイパーパラメータを設定する必要があります。

model = LeNet5(num_classes).to(device)

#Setting the loss function
cost = nn.CrossEntropyLoss()

#Setting the optimizer with the model parameters and learning rate
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

#this is defined to print how many steps are remaining when training
total_step = len(train_loader)

まず、クラスの数を引数として使ってモデルを初期化します。この場合は10です。次に、コスト関数をクロスエントロピー損失、オプティマイザーを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)
        
        #Forward pass
        outputs = model(images)
        loss = cost(outputs, labels)
        #Backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if (i+1) % 400 == 0:
            print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 

.format(epoch + 1, num_epochs, i + 1, total_step, loss.item())

コードが何をするのか見てみましょう:

  • 私たちは最初にエポックの数を反復処理し、その後トレーニングデータのバッチを処理します。
  • 私たちは使用しているデバイス、つまり GPU または CPU に応じて、画像とラベルを変換します。
  • 順伝播では、モデルを使用して予測を行い、その予測と実際のラベルに基づいて損失を計算します。
  • 次に、逆伝搬を行い、実際に重みを更新してモデルを改善します。
  • その後、optimizer.zero_grad() 関数を使用して、各更新の前に勾配をゼロに設定します。
  • 次に、loss.backward() 関数を使用して新しい勾配を計算します。
  • 最後に、optimizer.step() 関数で重みを更新します。

出力は次のように見ることができます。

ご覧の通り、損失はエポックごとに減少しており、これはモデルが実際に学習していることを示しています。この損失はトレーニングセットに基づいていることに注意してください。そして、損失が非常に小さい場合(私たちの場合のように)、それは過学習を示している可能性があります。この問題を解決する方法はいくつかありますが、例えば正則化やデータ拡張などがありますが、この記事ではそれについては触れません。それでは、モデルをテストして性能を見てみましょう。

モデルテスト

では、モデルをテストしてみましょう。

Test the model
In test phase, we don't need to compute gradients (for memory efficiency)
  
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()
    print('Accuracy of the network on the 10000 test images: {} %'.format(100 * correct / total))

ご覧のように、コードはトレーニング用のものとそれほど違いはありません。違いは、勾配を計算していない(with torch.no_grad()を使用)ことと、バックプロパゲーションを必要としないために損失を計算していない点です。モデルの結果としての精度を計算するには、正しい予測の総数を画像の総数で単純に計算すれば良いです。

このモデルを使用すると、約98.8%の精度が得られます。これは非常に良いです:

精度のテスト

MNISTデータセットは、今日の基準では非常に基本的で小さいものであり、他のデータセットでは同様の結果を得ることが難しいことに注意してください。それにもかかわらず、ディープラーニングやCNNを学ぶ際の良い出発点です。

結論

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

  • 私たちはLeNet5のアーキテクチャとその中にあるさまざまな種類の層について学ぶことから始めました。
  • 次に、私たちはMNISTデータセットを探求し、torchvisionを使用してデータをロードしました。
  • それから、モデルのハイパーパラメータを定義しながら、ゼロからLeNet5を構築しました。
  • 最後に、私たちはMNISTデータセットでモデルを訓練し、テストしました。モデルはテストデータセットで良好なパフォーマンスを発揮しているようでした。

未来の仕事

これはPyTorchにおける深層学習の非常に良い入門のようですが、この作業を拡張してさらに学ぶこともできます。

  • 異なるデータセットを試すことができますが、このモデルにはグレースケールデータセットが必要です。そうしたデータセットの一つがFashionMNISTです。
  • 異なるハイパーパラメータを試してみて、モデルにとって最適な組み合わせを見つけることができます。
  • 最終的に、データセットからレイヤーを追加または削除して、それがモデルの能力に与える影響を確認することができます。

関連記事: