PyTorchを使用した画像認識

カバー

[!] この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

PyTorchとは

PyTorchは、Lua言語で実装されたTorchというライブラリをもとに作られた、Python向けのオープンソース機械学習のライブラリです。
Facebook社の人工知能研究グループAI Research labにより開発され、2018年10月に安定版がはじめてリリースされた比較的新しいライブラリで、注目すべき機械学習ライブラリのひとつになっています。
この記事では、画像認識を題材にPyTorchのインストールからモデルの学習・予測を行います。

特徴

Kerasなどの機械学習用ライブラリと比較すると、以下のような特徴があります。

  1. 記述・判読がしやすくメンテナンス性に優れている
  2. NumPyの操作性に似ているため、操作しやすい
  3. ニューラルネットワークの構成をカスタマイズでき、オプティマイザーなど学習時の調整が可能
  4. 低レベルAPIであるため、デバックも簡単に行うことができる

環境構築

Pythonで動作するので、事前にPythonをインストールしてください。
この記事の開発環境は以下のとおりです。

バージョン
OSWindows 10 Pro
Python3.9.6

Pythonの仮想環境を作成

Pythonの開発では、パッケージのバージョン違いや依存関係が問題になることがよくあります。
そのため、仮想環境を作成して開発を行うことが一般的です。
ここでは「.venv」という名前の仮想環境を作成します。
以下のコマンドを実行して、仮想環境を作成し、有効化させます。

> python -m venv .venv
> ./.venv/Scripts/activate

以下のように表示されていれば、仮想環境が有効化されています。

(.venv) >

PyTorchのインストール

まずpipでPyTorchのインストールをします。

pip install torch torchvision

公式サイトで環境にあったPyTorchのインストール方法を確認できます。

使用するデータセット(CIFAR-10)

CIFAR-10とは、10クラス(airplane, automabile, bird, cat, deer, dog, frog, horse, ship, truck)のRGB画像(カラー画像)を含むデータセットです。
画像サイズは32×32ピクセルで、学習用データが5万枚、検証用データが1万枚の計6万枚のデータセットとなっています。
また、100クラスのデータを含むCIFAR-100というデータセットもあります。

画像認識の流れ

準備

必要なパッケージのインポート

必要なパッケージのインポートをします。

import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

transformを定義

PyTorchでデータを前処理する場合、あらかじめ用意されているtransformsパッケージを使用します。
transformsを利用することで簡単に画像の前処理ができるだけでなく、複数の前処理を同時に行ったり、 自分で定義した関数を使用できます。
またPyTorchで機械学習を行う際は、必ずTensorに変換する必要があります。
Tensorとは、PyTorchのデータを扱う際にもっとも基本となるデータ構造です。

今回は以下のクラスを使用して定義を行います。

  • ToTensor:PIL Image・Numpy配列をTensorに変換
  • Normalize:指定した平均・標準偏差でTensorを正規化
  • Compose:複数のTransformをまとめて実行
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))])

学習データと検証データの準備

CIFAR-10のデータをダウンロードします。
今回は./dataに保存しています。

# 学習データ
train_data_with_teacher_labels = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_data_loader = torch.utils.data.DataLoader(train_data_with_teacher_labels, batch_size=4, shuffle=True, num_workers=2)

# 検証データ
test_data_with_teacher_labels = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
test_data_loader = torch.utils.data.DataLoader(test_data_with_teacher_labels, batch_size=4, shuffle=False, num_workers=2)

torchvision.datasets.CIFAR10の引数は以下のとおりです。

  • root:保存ディレクトリ先を指定
  • train:Trueであれば学習用データとしてロード、Falseであれば検証用データとしてロードする
  • download:Trueの場合はデータセットをダウンロードし、rootで指定したディレクトリに配置する
  • transform:transformを指定

torch.utils.data.DataLoaderの引数は以下のとおりです。

  • batch_size:ミニバッチのサイズを指定
  • shuffle:Trueの場合エポックごとにデータをシャッフルする
  • num_workers:ミニバッチを作成する際の並行実行数を指定

モデル定義

学習のために畳み込みニューラルネットワークのモデルを定義します。
ニューラルネットワークは入力層・畳み込み層・プーリング層・全結合層・出力層から構成されています。

  • 入力層:ピクセルで構成されている画像データの入力
  • 畳み込み層:入力層で読み込んだ画像にフィルターをかけて特徴を検出する
  • プーリング層:圧縮処理を行う
  • 全結合層:入力層で検出された特徴を最後の出力層に渡す
  • 出力層:ソフトマックス関数を利用し、出力の確率の変換をおこなう

PyTorchのモデル定義

class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)  
        self.pool = nn.MaxPool2d(2, 2)  
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.layer1 = nn.Linear(16 * 5 * 5, 120)  
        self.layer2 = nn.Linear(120, 84)
        self.layer3 = nn.Linear(84, 10)

    def forward(self, input_data):
        input_data = self.pool(F.relu(self.conv1(input_data)))
        input_data = self.pool(F.relu(self.conv2(input_data)))
        input_data = input_data.view(-1, 16 * 5 * 5)
        input_data = F.relu(self.layer1(input_data))
        input_data = F.relu(self.layer2(input_data))
        input_data = self.layer3(input_data)
        return input_data

PyTorchでは、Init関数とForward関数の2つの関数が必要になります。

  • init:スーパーコンストラクターを呼び出し、学習・予測に必要なパラメーターの初期化を行う

    • nn.Conv2d:畳み込み層
      • 引数に入力チャンネル数、フィルター数、フィルタサイズを持つ
    • nn.MaxPool2d:プーリング層
      • 引数に領域のサイズ、ストライドを持つ
    • nn.Linear:全結合層
      • 引数に入力のベクトルサイズ、出力後のベクトルサイズを持つ
  • forward:実際の処理を書く

    • F.relu:活性化関数であり、各層の後に使用する

※ ほかのデータセットを使用する場合、パラメーターは変わります。

このように、PyTorchにおけるモデル定義の方法は、ほかの機械学習ライブラリとは大きく異なります。
たとえば、Kerasでのモデル定義は以下のようになります。

from keras.models import Sequential
from keras.layers import Conv2D
from keras.layers import Activation, Flatten, Dense

model = Sequential()
 
model.add(Conv2D(filters=10, kernel_size=(3, 3), padding='same', input_shape=(32,32,3)) activation='relu'))
model.add(Flatten())
model.add(Dense(10, activation='softmax'))
 
model.compile(loss='categorical_crossentropy', optimizer=Adam(), metrics=['accuracy'])

KerasではSequentialを使ったモデル定義の場合、model.add()で使用する層と必要なパラメーターを連続的に繋げるだけで、簡単に定義できることがわかります。

PyTorchはモデルの定義を自分で行う必要がありますが、層の種類や伝搬の仕方などの低レベルなところまで定義できます。

optimizerの設定

PyTorchには代表的なコスト関数や最適化手法はあらかじめ提供されています。

今回は以下を使用します。

  • コスト関数:クロスエントロピー
  • 最適化手法:SGD
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(params=model.parameters(), lr=0.001, momentum=0.9)

SGDの引数は以下のとおりです。

  • params:更新したいパラメーター
  • lr:学習率
  • momentum:過去の勾配が加わり、学習が不安定になる問題を防ぐ

学習

実際に動作させてみます。
今回は、ミニバッチ学習を行います。
ミニバッチ学習とは、学習用データからランダムにデータを取り出し、取り出したデータごとに勾配の計算とパラメーターの更新を行う学習方法です。
取り出した学習用データをミニバッチといい、すべてのミニバッチで学習したときの回数を1エポックといいます。
ミニバッチ学習では、学習の停滞を防ぐメリットがあります。

今回は4つずつデータを取り出し、3エポックで学習します。
学習用のデータは5万枚あるため、12500回学習が終了した場合1エポック終了することになります。

以下のコードをtorch_sample.pyとして保存します。

import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# エポック数
MAX_EPOCH = 3


# ニューラルネットワークの定義
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.layer1 = nn.Linear(16 * 5 * 5, 120)
        self.layer2 = nn.Linear(120, 84)
        self.layer3 = nn.Linear(84, 10)

    def forward(self, input_data):
        input_data = self.pool(F.relu(self.conv1(input_data)))
        input_data = self.pool(F.relu(self.conv2(input_data)))
        input_data = input_data.view(-1, 16 * 5 * 5)
        input_data = F.relu(self.layer1(input_data))
        input_data = F.relu(self.layer2(input_data))
        input_data = self.layer3(input_data)
        return input_data


def main():

    # transform定義
    transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

    # 学習データ
    train_data_with_teacher_labels = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
    train_data_loader = torch.utils.data.DataLoader(train_data_with_teacher_labels, batch_size=4, shuffle=True, num_workers=2)

    model = CNN()

    # optimizerの設定
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(params=model.parameters(), lr=0.001, momentum=0.9)

    # 学習
    for epoch in range(MAX_EPOCH):
        total_loss = 0.0
        for i, data in enumerate(train_data_loader, 0):
            # 学習データと教師ラベルデータを取得
            train_data, teacher_labels = data
            # 勾配情報を削除
            optimizer.zero_grad()
            # モデルで予測を計算
            outputs = model(train_data)
            # 微分計算
            loss = criterion(outputs, teacher_labels)
            loss.backward()
            # 勾配を更新
            optimizer.step()
            # 誤差
            total_loss += loss.item()
            # 1000ミニバッチずつ進捗を表示
            if i % 1000 == 999:
                print('学習進捗:[学習回数:%d, ミニバッチ数:%5d] loss: %.3f' % (epoch + 1, i + 1, total_loss / 1000))
                total_loss = 0.0
                
    # モデル保存
    torch.save(model.state_dict(), "model.pth")

    print("-----学習完了-----")

以下のコマンドを実行して学習させます。

(.venv) > python torch_sample.py

実行すると以下のように進捗状況がターミナルに出力されます。

学習進捗:[学習回数:1, ミニバッチ数:  1000] loss: 2.300
学習進捗:[学習回数:1, ミニバッチ数:  2000] loss: 2.170
学習進捗:[学習回数:1, ミニバッチ数:  3000] loss: 1.965
学習進捗:[学習回数:1, ミニバッチ数:  4000] loss: 1.865
学習進捗:[学習回数:1, ミニバッチ数:  5000] loss: 1.745
学習進捗:[学習回数:1, ミニバッチ数:  6000] loss: 1.690
学習進捗:[学習回数:1, ミニバッチ数:  7000] loss: 1.641
学習進捗:[学習回数:1, ミニバッチ数:  8000] loss: 1.615
学習進捗:[学習回数:1, ミニバッチ数:  9000] loss: 1.543
学習進捗:[学習回数:1, ミニバッチ数: 10000] loss: 1.541
学習進捗:[学習回数:1, ミニバッチ数: 11000] loss: 1.506
学習進捗:[学習回数:1, ミニバッチ数: 12000] loss: 1.498
学習進捗:[学習回数:2, ミニバッチ数:  1000] loss: 1.404
学習進捗:[学習回数:2, ミニバッチ数:  2000] loss: 1.434
学習進捗:[学習回数:2, ミニバッチ数:  3000] loss: 1.410
学習進捗:[学習回数:2, ミニバッチ数:  4000] loss: 1.398
学習進捗:[学習回数:2, ミニバッチ数:  5000] loss: 1.340
学習進捗:[学習回数:2, ミニバッチ数:  6000] loss: 1.360
学習進捗:[学習回数:2, ミニバッチ数:  7000] loss: 1.334
学習進捗:[学習回数:2, ミニバッチ数:  8000] loss: 1.337
学習進捗:[学習回数:2, ミニバッチ数:  9000] loss: 1.296
学習進捗:[学習回数:2, ミニバッチ数: 10000] loss: 1.333
学習進捗:[学習回数:2, ミニバッチ数: 11000] loss: 1.294
学習進捗:[学習回数:2, ミニバッチ数: 12000] loss: 1.308
学習進捗:[学習回数:3, ミニバッチ数:  1000] loss: 1.240
学習進捗:[学習回数:3, ミニバッチ数:  2000] loss: 1.218
学習進捗:[学習回数:3, ミニバッチ数:  3000] loss: 1.226
学習進捗:[学習回数:3, ミニバッチ数:  4000] loss: 1.230
学習進捗:[学習回数:3, ミニバッチ数:  5000] loss: 1.236
学習進捗:[学習回数:3, ミニバッチ数:  6000] loss: 1.264
学習進捗:[学習回数:3, ミニバッチ数:  7000] loss: 1.210
学習進捗:[学習回数:3, ミニバッチ数:  8000] loss: 1.206
学習進捗:[学習回数:3, ミニバッチ数:  9000] loss: 1.203
学習進捗:[学習回数:3, ミニバッチ数: 10000] loss: 1.209
学習進捗:[学習回数:3, ミニバッチ数: 11000] loss: 1.180
学習進捗:[学習回数:3, ミニバッチ数: 12000] loss: 1.185
-----学習完了-----

予測

学習したモデルを読み込み、検証を行います。
今回はクラスごとの精度を検証してみます。

import torch
import torchvision
import torchvision.transforms as transforms
from torch_sample import CNN

def main():

    # モデル読み込み
    model = CNN()
    model.load_state_dict(torch.load("model.pth"))

    # transform定義
    transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

    # 検証データ
    test_data_with_teacher_labels = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
    test_data_loader = torch.utils.data.DataLoader(test_data_with_teacher_labels, batch_size=4, shuffle=False, num_workers=2)

    # クラスの中身を設定
    class_names = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

    # クラスごとの検証結果
    class_corrent = list(0. for i in range(10))
    class_total = list(0.for i in range(10))

    with torch.no_grad(): # 勾配の計算をしない
        for data in test_data_loader:
            # 検証データと教師ラベルデータを取得
            test_data, teacher_labels = data
            # 検証データをモデルに渡し予測
            results = model(test_data)
            # 予測結果を取得
            _, predicted = torch.max(results, 1) 
            c = (predicted == teacher_labels).squeeze()
            for i in range(4):
                label = teacher_labels[i]
                class_corrent[label] += c[i].item()
                class_total[label] += 1
    # 結果表示
    for i in range(10):
        print(' %5s クラスの正解率: %2d %%' % (class_names[i], 100 * class_corrent[i] / class_total[i]))

結果は以下のとおりになりました。

 plane クラスの正解率: 48 %
   car クラスの正解率: 82 %
  bird クラスの正解率: 43 %
   cat クラスの正解率: 22 %
  deer クラスの正解率: 32 %
   dog クラスの正解率: 66 %
  frog クラスの正解率: 75 %
 horse クラスの正解率: 58 %
  ship クラスの正解率: 68 %
 truck クラスの正解率: 56 %

今回はPyTorchの特徴に関する解説のため、正解率向上の方法については省略します。

まとめ

今回はPyTorchで一からニューラルネットワークを構成し、学習・予測を行いました。
ほかのライブラリと異なり、ニューラルネットワークの定義・optimizerの定義を柔軟に扱うことができ、低レベルのパラメーターの変更も簡単にできます。
たとえば、ニューラルネットワークの構成を変更したい場合は、 モデルの定義で定義したクラスのinitに層を追加したり、forwardで伝搬の仕方を変更できます。
はじめての方も他のライブラリを使ったことがある方も、ぜひ一度PyTorchを使ってみてください。

参考文献

  • [1]: PyTorch, https://pytorch.org/
  • [2]: 画像認識プログラミングレシピ、川島賢、秀和システム、2020

TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。