PythonでのAI開発をはじめてみよう(PyTorchでMNIST)

PyToarchを使って、MNISTの学習および推論を実践します

images/cards/PyTorch_logo_black.svg.webp

目次

1. まずは、開発環境を準備

ここでは、Ubuntu 環境を想定して解説します。もちろん Windows でも問題ありません。

  • Ubuntu 20.04
  • GeForce RTX 2080 Ti
  • CUDA Version: 11.5
  • Python 3.8

2. PyTorch のインストール

PyTorch とは

PyTorch は、オープンソースの AI ライブラリです。Meta の The Fundamental AI Research (FAIR) team によって開発されました。ずっと Tensorflow(Keras)を使っていましたが、Pytorch の人気が上昇しており、今回採用しています。PyTorch は、「Define by Run」という方針で設計されています。

  • Define by Run
    • データを流しながら、計算グラフの構造を決定
    • 動的な計算グラフとなり、デバッグがしやすい(途中の値が参照など)。
  • Define and Run
    • TensorFlow Ver. 1.15.0(Ver.2.0.0)までがこの方式を採用。 ※Ver.2.0.0 移行は、Define By Run。
    • 計算グラフの構造を予め定義しておき、そこにデータが流される

インストール方法

PyTorch のページSTART LOCALLYから、インストール方法を選択してください。 選択の画面に、自分の PC の環境に当てはまるボタンを押すと「Run this Command:」欄にインストールコマンドが表示されます。

PyTorchのインストール

Pipenv が便利

「Pipenv」は、Pipfile に対してパッケージの追加および削除を行い、自動でプロジェクト用の仮想環境を作成し管理します。 Node.js の npm のようなものです。Python では、パッケージのインストールは pip、仮想環境の構築なら virtualenv(venv)をよく使いますが、pipenv はそれらをまとめて簡単に扱えるようにしています。

Tips

pipenv は、初期設定では、「~/.local/share/virtualenvs/」の下に仮想環境を作ります。 環境変数の「PIPENV_VENV_IN_PROJECT」を true にすることで、プロジェクトの直下に仮想環境を作ることができます。 「PIPENV_VENV_IN_PROJECT」を定義すると、プロジェクトの直下に 「.venv」フォルダが作られ、パッケージ類は全てそこにインストールされます。

pip install pipenv
export PIPENV_VENV_IN_PROJECT=true
mkdir pytorch
cd pytorch
pipenv --python 3.8
pipenv install torch torchvision torchaudio

Python を起動して、以下のコマンドを入力し、import エラー等が表示されなければ、準備完了です。

python
>>> import torch
>>> print(torch.cuda.is_available())
>>> x = torch.rand(5, 3)
>>> print(x)

3. 教師データ(MNIST)を準備

MNIST とは

MNIST データベースは、様々な画像処理システムの学習に広く使用される手書き数字画像の大規模なデータベースです。 米国商務省配下の研究所が構築したこのデータベースは、機械学習分野での学習や評価に広く用いられています。 「torchvision」 の中でも、データセットとして提供されています。 以下のように、引数 download を True とすれば、データがない場合に、自動的にダウンロードされます。

まずは、学習用のデータセットと評価用のデータセットを以下のように設定します。

train_dataset = datasets.MNIST(
    './data',               # データの保存先
    train = True,           # 学習用データを取得する
    download = True,        # データが無い時にダウンロードする
    transform = transforms.Compose([transforms.ToTensor()])  # テンソルへの変換など
    )
test_dataset = datasets.MNIST(
    './data',
    train = False,
    transform = transforms.Compose([transforms.ToTensor()])  # テンソルへの変換など
    )

Dataloader とは

AI 学習で用いられる「Dataloader」とは、用意したデータセットからバッチサイズ毎にデータを取り出すためのローダーになります。 データセットからいくつかのデータを取得して、ミニバッチを作成するクラスです。基本的に、「torch.utils.data.DataLoader」が使われます。 このクラスに、先ほど作成した train_dataset と test_dataset を渡すと共に、取り出すバッチサイズも指定します。

train_dataloader = torch.utils.data.DataLoader(
    train_dataset,
    batch_size = num_batch,
    shuffle = True)
test_dataloader = torch.utils.data.DataLoader(
    test_dataset,
    batch_size = num_batch,
    shuffle = True)

4. 数字判別用のニューラルネットワークを定義

MNIST の手書き画像を入力して、1 ~ 9 までの数字を判別するニューラルネットワーク構造を定義します。

torch.nn.Linear

入力データに対して線形変換を施す関数であり、ニューラルネットワークの文脈では、全結合(層)となる。 ※tensorflow.keras では、Dense(層)に相当する。

ネットワーク構造

forward 関数には、順伝播の流れが記載されています。合計 3 層のシンプルなニューラルネットワーク構造になっています。

  1. fc1: 入力層のサイズから中間層(隠れ層)の サイズ(100) に変換する
  2. torch.sigmoid: シグモイド関数を活性化関数として活性化させる
  3. fc2: 中間層(隠れ層)のサイズ(100)から出力層(1 ~ 9)のサイズ(9) に変換する
  4. F.log_softmax: ログソフトマックス関数を活性化関数として活性化させ、出力とする。
class Net(nn.Module):
    def __init__(self, input_size, output_size):
        super(Net, self).__init__()

        # 各クラスのインスタンス(入出力サイズなどの設定)
        self.fc1 = nn.Linear(input_size, 100)
        self.fc2 = nn.Linear(100, output_size)

    def forward(self, x):
        # 順伝播の設定(インスタンスしたクラスの特殊メソッド(__call__)を実行)
        x = self.fc1(x)
        x = torch.sigmoid(x)
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

# ニューラルネットワークの生成
model = Net(image_size, 10).to(device)

Tips

PyTorch はテンソルに対して .to(device)などで簡単に cpu と gpu モードを切り替えられます。

  • model.to(“cuda”)とすると、モデルが GPU に転送され処理されます。
  • model.to(“cpu”)とすると、モデルが CPU に転送され処理されます。

5. 損失関数の設定

損失関数とは

「正解値」と、モデルによる出力された「予測値」とのズレの大きさ(このズレを Loss(損失)と呼ぶ)を計算するための関数です。 この損失の値を最小化するように、モデルは学習されます。ズレの大きさを計算する関数としては、二乗誤差(MSE など)が用いられますが、 交差エントロピー(CrossEntropy)、確率分布 p と確率分布 q の近似性を表現する関数が用いられることが多いです。この関数を用いた方が、学習のスピードが早いからです(1 回の学習あたりの損失関数の減少幅が大きい)。

criterion = nn.CrossEntropyLoss()

6. 最適化手法の設定

最適化手法(Optimizer)とは

学習を行う際に Loss(損失)を限りなくゼロにするために、最適な解を導き出すためのアルゴリズムや方法のことである。 最急降下法、SGD、Momentum、RMSProp、Adam などがよく知られている。 Adam(Adaptive Moment Estimation)は「Momentum」と「Adagrad」の融合というアイデアにより考案されました。

学習率とは

学習を行う際に Loss(損失)を限りなくゼロにするために、各反復でステップサイズを決定します。これは、新しく取得した情報が古い情報をどの程度上書きするかに影響を与えるため、モデルが「学習」する速度を比喩的に表します。学習率を大きくしすぎると発散し、小さくしすぎると収束まで遅くなります。学習が進むにつれて、徐々に小さくしていく方法が用いられます。

optimizer = torch.optim.Adam(model.parameters(), lr = learning_rate)

7. いざ学習!

エポック数とは

一つの訓練データに対して何回繰り返して学習させるか、その回数を表しています。深層学習のようにパラメータの数が多いものになると、訓練データを何回も繰り返して学習させないとパラメータをうまく学習できません。逆に、学習し過ぎると、過学習となる場合があります。

どれくらいのエポック数が最適なのか?

評価データに対して予測精度が落ちてきて、これ以上学習を進めると過学習(Over fitting)してしまうという段階で学習を打ち切るのが最適です。これを「Early Stopping」と言います。 その時のエポック数が適当なエポック数となります。

PyTorch と Tensorflow(Keras)の違い

Tensorflow(Keras)では、model.fit で学習をお任せでしたが、PyTorch では for 文を使って自分で反復させます。 この辺りが動的に計算される「Define by Run」の違いなのかもしれません。

  1. Dataloader からバッチサイズ分のデータ(inputs, labels)を取り出します。

    • inputs: 入力画像データ(手書きの画像データ)
    • labels: 1 ~ 9 までのデータ(正解値)
  2. Optimizer を初期化します

  3. inputs を定義したニューラルネットワークに入力します

  4. ニューラルネットワークの出力(予測値)と、labels(成果値)のズレを損失関数で計算します

  5. 計算した損失から誤差逆伝播法により、その勾配を計算します

  6. 各ニューロンのパラメータを修正します

model.train() # モデルを訓練モードにする

for epoch in range(num_epochs): # 学習を繰り返し行う
  loss_sum = 0

    for inputs, labels in train_dataloader:

        # 1. データを取り出す
        inputs = inputs.to(device)
        labels = labels.to(device)

        # 2. Optimizerを初期化
        optimizer.zero_grad()

        # 3. ニューラルネットワークの処理を行う
        inputs = inputs.view(-1, image_size) # 画像データ部分を一次元へ並び変える
        outputs = model(inputs)

        # 4. 損失(出力とラベルとの誤差)の計算
        loss = criterion(outputs, labels)
        loss_sum += loss

        # 5. 勾配の計算
        loss.backward()

        # 6. 重みの更新
        optimizer.step()

    # 学習状況の表示
    print(f"Epoch: {epoch+1}/{num_epochs}, Loss: {loss_sum.item() / len(train_dataloader)}")

    # モデルの重みの保存
    torch.save(model.state_dict(), 'model_weights.pth')

8. 評価をしてみよう!

評価用データを使って、学習したモデルを評価してみましょう。

model.eval()  # モデルを評価モードにする

loss_sum = 0
correct = 0

with torch.no_grad():
    for inputs, labels in test_dataloader:

        # GPUが使えるならGPUにデータを送る
        inputs = inputs.to(device)
        labels = labels.to(device)

        # ニューラルネットワークの処理を行う
        inputs = inputs.view(-1, image_size) # 画像データ部分を一次元へ並び変える
        outputs = model(inputs)

        # 損失(出力とラベルとの誤差)の計算
        loss_sum += criterion(outputs, labels)

        # 正解の値を取得
        pred = outputs.argmax(1)
        # 正解数をカウント
        correct += pred.eq(labels.view_as(pred)).sum().item()

print(f"Loss: {loss_sum.item() / len(test_dataloader)}, Accuracy: {100*correct/len(test_dataset)}% ({correct}/{len(test_dataset)})")

参考

関連記事