DeepLearning pytorch

【初心者向け】PyTorch ディープラーニング実装の基本フロー

昨今、様々なOSSのディープラーニングフレームワークがあり気軽に利用できます。有名どころですとGoogleのTensorFlowKeras、そしてPyTorchでしょうか。

最近ではPyTorchのトレンド上昇が目立つみたいですね。といってもまだTensorFlowやKerasの方が人気のようですが、次第にPyTorchユーザーが増えているようです。Google Colaboratoryでもデフォルトでインストールされておりすぐ使えるようになっています。

このブログではPyTorchをメインに扱っていこうかなと思います。

PyTorchの特徴はDefine by Run型でデータをネットワークモデルで順番に処理していくなかで計算グラフを構築するものです。これにより柔軟なネットワークモデルの構築や途中の計算結果取り出しが行えます。

さて、そんなPyTorchですが実装するとなるとすこしばかし独自のお作法があります。なので、今回はPyTorchにおけるディープラーニング実装の基本フローを備忘録として残したいと思います。

なお、本記事の内容はこちらの書籍「つくりながら学ぶ!PyTorchによる発展ディープラーニング」を参考にしております。

実装フローの概観

まずは実装の全体の流れです。おおまかなフローとしては、以下のようになります。

  • データ準備
  • ネットワークモデルの定義
  • 学習のための設定
  • 学習・検証の実施
  • テストデータで推論
PyTorchディープラーニング実装フロー

それでは、順を追って説明していきます。

なお、実装コードはGithubのレポジトリpometa0507/pytorch-implementation-flowにアップしています。ノートブックはこちらからアクセスできます。たまにGitHubの調子が悪くノートブックを表示できない場合がありますので、その際はnbviewerのこちらにアクセスいただければ表示できるかと思います。

データ準備

まずはデータがないことには始まりません。ということでデータを用意するところから始めます。

0. データを用意

データの種類は画像や音声、言語データなどいろいろありますが、今回はPyTorchがすでに用意してくれているデータセットMNISTの手書き数字データの分類を題材に説明していきます。

MNISTのデータセットであれば、PyTorchでデフォルトに用意されている関数を使って後述するDatasetを簡単に設定することができます。

ちなみに、自前でデータを用意した場合は自作Datasetを作成する必要があります。例えば、JPGなどの画像データファイルからDatasetを作成するには、torchvision.datasets.ImageFolderを使うことで、クラスごとに分かれて画像ファイルが格納されているrootディレクトリ(+前処理のtransform)を指定することでDatasetを作成することができます。

補足ですが、MNISTのデータセットについて簡単に説明しておきます。MNISTは「0」から「9」までの10種類の手書き数字の画像データセットです。

MNISTデータセットの一部

上図はMNISTデータセットの一部を図示したものです。一枚の画像データの形状は、Height = 28, Width = 28 であり、グレースケールのため Channel = 1となっています。

1. 前処理、モデルの入出力を確認

データは前処理を行ってからネットワークモデルに入力します。

そのため、まずは前処理の内容を確認します。

今回はPyTorchで用意されているMNISTのDatasetを使いますので必要な前処理は下記だけとなります。

  • ロードしたデータをPyTorchのTensor型に変換
  • 標準化

ちなみに、画像ファイルの前処理であれば下記のような処理を行ったりします。

  • JPGファイルをPIL(pillow)にロード
  • PILからPyTorchのTensor型に変換
  • 標準化
  • データ拡張

次にネットワークモデルの入出力を確認します。

PyTorchで画像データを扱う場合、ネットワークモデルで処理するデータ形状はチャンネルファーストとなります。つまり(Channel, Height, Width)の順番です。ndaarayやPIL形式の場合、(Height, Width, Cannel)なので注意が必要です。といっても前処理の「Tensor型に変換」により自動的にチャネルファーストに変換してくれます。

また、ネットワークモデルでは(ミニ)バッチのデータに対して処理を行うので、モデルに入力するデータ形状は先頭にバッチサイズNの軸を追加して(N, C, H, W)となります。

例として、MNIST画像データとモデルの入出力関係を図示します。

ネットワークモデルの入出力

図中では、バッチサイズを5としてモデルに入力しています。(モデルとしてはバッチサイズはいくつでも入力できますが、あくまで図中の説明ではバッチサイズを5としています。)

MNIST分類は10クラス分類なので、モデルの出力は(バッチサイズ、クラス数の10)となります。

(補足)バッチサイズNの軸は、DataLoaderからデータを取り出す際に自動的に追加されますので、Datasetで取り出されるデータ形状はバッチサイズNの軸がない形状の(C, H, W)とします。

以上で前処理とモデルの入出力が確認できました。

2. Datasetの作成

次にDatasetの作成についてです。

Datasetとは文字通りデータの集まりですが、PyTorchにおけるDatasetはデータに前処理を自動で適用し、データを1つずつ取り出すことができます。

教師あり学習の場合は、Datasetにデータとラベルをセットにして格納し、取り出すときもデータとラベルをセットで取り出すことができます。

Datasetの作成は次の手順で行います。

  1. 前処理 transform を設定する
  2. transform を Dataset に適用して作成する

また、Datasetは学習に使用する訓練用Datasetと評価に使用する検証用Datasetを2つ作成します。

実際のコードを見てDataset作成の方法を確認していきます。

が、その前に今回使用するモジュールを一式インポートしておきます。

# モジュールのインポート
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms

import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm

下記がMNISTの訓練用と検証用のDatasetを作成する一連のコードです。

#データ前処理 transform を設定
transform = transforms.Compose(
    [transforms.ToTensor(),                      # Tensor変換とshape変換 [H, W, C] -> [C, H, W]
     transforms.Normalize((0.5, ), (0.5, ))])    # 標準化 平均:0.5  標準偏差:0.5

#訓練用Datasetを作成
train_dataset = datasets.MNIST(root='./data', 
                                        train=True,
                                        download=True,
                                        transform=transform)


#検証用Datasetを作成
val_dataset = datasets.MNIST(root='./data', 
                                        train=False, 
                                        download=True, 
                                        transform=transform)

データ前処理の transform を設定しています。

今回の前処理は、「Tensor型に変換」と「標準化」を行います。

torchvision.transforms.Composeを使うことで、複数の前処理をリストに定義した順番で実行してくれます。下記のtransformを定義することで、Tensor型に変換と標準化を一気に処理してくれます。

最後に訓練用と検証用のDatasetを作成しています。

MNISTなど有名なデータセットはPyTorchで用意されている関数torchvision.datasets.MNIST()を使って簡単にダウンロードしてDatasetの作成ができます。

Datasetの作成するときに、事前に定義しておいた前処理を引数transformに設定してやります。

以上でMNISTのDatasetを作成することができました。

3.DataLoaderの作成

DatasetをもとにDataLoaderを作成します。

#訓練用 Dataloder
train_dataloader = torch.utils.data.DataLoader(train_dataset,
                                            batch_size=64,
                                            shuffle=True)

#検証用 Dataloder
val_dataloader = torch.utils.data.DataLoader(val_dataset, 
                                            batch_size=64,
                                            shuffle=False)

# 辞書型変数にまとめる
dataloaders_dict = {"train": train_dataloader, "val": val_dataloader}

torch.utils.data.DataLoaderでDataLoaderを作成できます。引数には、Datasetとバッチサイズ、そしてシャッフルの有無を指定します。

学習時に扱いやすくするため辞書型変数dataloaders_dictに各Dataloaderを格納しています。

ネットワークモデルの定義

次はネットワークモデルの定義です。

4. モデルの作成5. 順伝播の定義

今回は畳み込み層と全結合層を組み合わせたモデルを構築します。

下記にコードを示します。

# 畳み込み層+全結合層のネットワークモデル
class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1)    #畳み込み層
        self.conv2 = nn.Conv2d(32, 64, 3, 1)   #畳み込み層
        self.fc1 = nn.Linear(9216, 128)        #全結合層
        self.fc2 = nn.Linear(128, 10)          #全結合層

    def forward(self, x):
        x = self.conv1(x)              # (Batch,  1, 28, 28) -> (Batch, 32, 26, 26)
        x = F.relu(x)
        x = self.conv2(x)              # (Batch, 32, 26, 26) -> (Batch, 64, 24, 24)
        x = F.relu(x)
        x = F.max_pool2d(x, 2)         # (Batch, 64, 24, 24) -> (Batch, 64, 12, 12)
        x = torch.flatten(x, 1)        # (Batch, 64, 12, 12) -> (Batch, 9216)
        x = self.fc1(x)                # (Batch, 9216) -> (Batch, 128)
        x = self.fc2(x)                # (Batch, 128) -> (Batch, 10)

        return x

#モデル作成
net = Net()

Pytorchのモデルはtorch.nn.Moduleクラスを継承して作成します。class Net(nn.Module): としてクラスを作成しています。

def __init__(self):で各層(レイヤー)を定義し、 def forward(self, x) で順伝播の処理を定義します。ここの引数xはモデルに入力するTensorとなります。今回の場合、入力xの形状は(Batch, 1, 28, 28)です。

最後に、Netクラスをインスタンス化してモデルを作成しています。

以上でネットワークモデルの定義と作成が完了です。

学習のための設定

次は学習のための設定です。

モデルだけ作成しても学習はできないですからね。

6.損失関数の定義

今回は損失関数に torch.nn.CrossEntropyLoss() を使用します。

こちらは、Softmax関数+交差エントロピー誤差の処理となっています。(損失関数にSoftmax関数が組み込まれているので、モデルでは出力層にSoftmax関数を追加していませんでした!)

交差エントロピー誤差は分類問題での損失関数となります。

以下、コードです。

# nn.CrossEntropyLoss() はソフトマックス関数+クロスエントロピー誤差
criterion = nn.CrossEntropyLoss()

7.最適化手法の設定

次に最適化手法の設定です。

最適化は誤差逆伝播で勾配を計算したあとに実行されます。引数に渡している net.parameters() に勾配の情報が格納されています。

今回は Adam を使用します。学習率 lr は0.001としました。

# 最適化関数Adam
optimizer = optim.Adam(net.parameters(), lr=0.001)

これで学習の準備が整いました。

8. 学習・検証・テスト

それでは学習と検証実施するコードを示します。

# モデルを学習させる関数を作成
def train_model(net, dataloaders_dict, criterion, optimizer, device, num_epochs):
    
    # epochのループ
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch+1, num_epochs))
        print('-------------')

        # epochごとの学習と検証のループ
        for phase in ['train', 'val']:
            if phase == 'train':
                net.train()  # モデルを訓練モードに
            else:
                net.eval()   # モデルを検証モードに

            epoch_loss = 0.0  # epochの損失和
            epoch_corrects = 0  # epochの正解数

            # 未学習時の検証性能を確かめるため、epoch=0の訓練は省略
            if (epoch == 0) and (phase == 'train'):
                continue

            # データローダーからミニバッチを取り出すループ
            for i , (inputs, labels) in tqdm(enumerate(dataloaders_dict[phase])):

                # optimizerを初期化
                optimizer.zero_grad()

                # 順伝搬(forward)計算
                with torch.set_grad_enabled(phase == 'train'):  # 訓練モードのみ勾配を算出
                    outputs = net(inputs)              # 順伝播
                    loss = criterion(outputs, labels)  # 損失を計算
                    _, preds = torch.max(outputs, 1)   # ラベルを予測
                    
  
                    # 訓練時はバックプロパゲーション
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                    # イタレーション結果の計算
                    # lossの合計を更新
                    epoch_loss += loss.item() * inputs.size(0)  
                    # 正解数の合計を更新
                    epoch_corrects += torch.sum(preds == labels.data)

            # epochごとのlossと正解率を表示
            epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset)
            epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset)
            
            print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))

# 学習・検証を実行する
num_epochs = 3
train_model(net, dataloaders_dict, criterion, optimizer, device, num_epochs=num_epochs)

学習は epock ごとのループとepock内でバッチを取り出してイテレータごとに処理するループ2重ループ構造になっています。

イメージは下図みたいな感じです。Dataloaderからバッチを取り出しています

with torch.set_grad_enabled(phase == 'train'): は学習時にのみ勾配の算出を有効にする設定です。評価する時は勾配を計算する必要がないので無効となります。

学習は基本的に以下の流れで行います。コードにもこの流れが実装されています。

  1. optimizer.zero_grad() #optimizerを初期化
  2. outputs = net(inputs) # 順伝播で出力を得る
  3. loss = criterion(outputs, labels) # 損失を計算
  4. loss.backward() #誤差逆伝播で勾配を計算
  5. optimizer.step() #最適化でモデルの重みを更新

今回のコードではepock数は3として学習させています。

以下は学習の過程です。学習前は正解率が9.0%程度ですが、学習後は98.5%にまで上昇していることがわかります。うまく学習できたようです。

9.テストデータで推論

最後にテストデータで推論を行います。

簡単のため、今回はテストデータとして検証用データを流用します。

#テストデータで推論
batch_iterator = iter(dataloaders_dict["val"])  # イテレータに変換
imges, labels = next(batch_iterator)  # 1番目の要素を取り出す

net.eval() #推論モード
with torch.set_grad_enabled(False):   # 推論モードでは勾配を算出しない
    outputs = net(imges)               # 順伝播
    _, preds = torch.max(outputs, 1)  # ラベルを予測
    
#テストデータの予測結果を描画
plt.imshow(imges[0].numpy().reshape(28,28), cmap='gray')
plt.title("Label: Target={}, Predict={}".format(labels[0], preds[0].numpy()))
plt.show()

正解ラベル7に対して、正しく7と分類できていることがわかります。

おわりに

すこし長くなりましたが、以上がPyTorchのディープラーニング実装の基本フローとなります。

他のモデルを構築する際にもこのフローを参考に流用いただけると幸いです。

以上です。ありがとうございました。

-DeepLearning, pytorch
-

Copyright© Program as Life , 2023 All Rights Reserved Powered by AFFINGER5.