PyTorch による手書き数字分類の第一歩

# PyTorchによる手書き数字分類

> この記事は Deep Learning エンジニアの [Dominic Monn （@dqmonn）](https://twitter.com/dqmonn) 氏が [TECH x GAME COLLEGE](https://techxgamecollege.connpass.com/) のために寄稿していただいたものをQiita用にリライトしたものです。

## イントロダクション

このQiitaの記事では、PyTorchというフレームワークと畳込みニューラルネットワークを使い、たった100行に満たないコードでどのように手書き文字認識を実現するのかについて1から紹介します。

最近のフレームワークを使えば、実用可能なニューラルネットワークを構築することは数年前と比べ大分簡単になっています。ヘルパー関数を使うことによって、どのようなネットワークでも簡単に構築したり読み込ませる事が出来ます。データを読み込ませ、ネットワークを最適化することも出来ます。それでは、さっそく始めてみましょう。

このチュートリアルで使用されている全てのコードは [https://github.com/tech-x-college/pytorch-first-step](https://github.com/tech-x-college/pytorch-first-step) に公開されています。

## 歴史

これから紹介するいくつかのステップで、畳み込みニューラルネットワークとMNISTのデータセットを使った、手書き数字認識スクリプトを作成していきます。

![A subset of the MNIST dataset](https://raw.githubusercontent.com/tech-x-college/pytorch-first-step/master/MNIST.png)

この問題と使用するデータセットはコンピュータービジョンの分野において極めて古い問題です。MNISTデータセットは90年代に研究者達によって集められ、それ以来、この問題における人気データセットとして利用され続けています。似たようなデータセットが90年台に、手紙を仕分けする目的や銀行小切手を分類することを目的とした商用文字認識機を作るために使われています。かつての認識率は大体60％程でしたが、今日における認識率は人間レベルのパフォーマンスであるほぼ100％まで到達しています。それでは、これから私達の構築する数字認識がどれくらいのパフォーマンスを達成するのか、さっそく見ていきましょう!

## 動作環境
```
$ python -V
Python 3.5.1
```

- **macOSは2系がデフォルトでインストールされており、動作しないので注意**
- [MacOSへPython3をインストール](https://www.python.jp/install/macos/index.html#python3)
- [WindowsへPython3をインストール](https://www.python.jp/install/windows/install_py3.html)

## PyTorchのセットアップ

PyTorchのセットアップは至って簡単です。始めるにあたって必要なことは:

- Pythonがパソコンにインストールされていること（MacOSとLinuxには予めプリインストールされています）。
- データセットとソースコードをダウンロードするためのインターネット環境があること。
- もし、より早く動作するバージョンのPyTorchを使いたいのであれば、GPUを使用するためにCudaをインストールする必要があります。しかしながら、このチュートリアルでは特に必要ないのでスキップします。

もし上記の条件が全てそれっていれば、後はただターミナルを開き、`pip install torch torchvision` のコマンドをコピーするだけです。

数秒でダウンロードが終了します。これだけで全てのセットアップが終了です。モデルの構築に必要な準備は全て整いました！

## MNIST dataの読み込み

前もって話していたように、MNISTは長い間使われ続けている有名なデータセットであるため、現在使用されている大半のフレームワークで簡単にデータを読み込む方法が準備されています。PyTorchも例外ではありません。

MNISTデータセットをこのトレーニングで使うためには、以下のステップが必要です:

- データをダウンロードする。
- uniform/normalizedフォームを使用する（違うフォームを使うとネットワークが混乱しています！）。
- そのデータをPyTorchが分割して扱えるような形でロードする(データセットをそのまま使ってしまうと、メモリが足りなくなってしまいます)。

これらの作業は数年前ではとても大変なことでした。しかし今では、以下のコードを実行するだけで簡単にデータをダウンロードすることが出来ます。

```
from torchvision import datasets, transforms

datasets.MNIST('../data', train=True, transform=transforms.Compose([
                   transforms.ToTensor(),
                   transforms.Normalize((0.1307,), (0.3081,))
               ]))
```

`datasets.MNIST` 関数はデータセットをローカルディレクトリにダウンロードし、このトレーニングで使いやすいような形に変換と正規化をしてくれます（関数内の `transform` 引数で指定されているように）。

`Normalize` の呼び出しに使われている数字は、この操作によく使われる定数です。いくつかのまとまりに分割には他のヘルパークラスである `torch.utils.data.DataLoader` を使用します。ロード手順の概観は次のような形になります。

```
import torch
from torchvision import datasets, transforms

train_loader = torch.utils.data.DataLoader(
        datasets.MNIST('../data', train=True, download=True,
                       transform=transforms.Compose([
                           transforms.ToTensor(),
                           transforms.Normalize((0.1307,), (0.3081,))
                       ])),
        batch_size=64, shuffle=True)
```

先程使った `datasets.MNIST` 関数がこの新しい `torch.utils.data.DataLoader` クラスにおいてどのように使われているか確認してみてください。これは、私達のデータをシャッフル、分解し、トレーニングに対しての準備を整えてくれています。

## テストデータ

この `train=True` フラグの存在に気づいたでしょうか。実際のケースでは、訓練データセットとテストデータセットをそれぞれ用意します。訓練データは全体の約90%程のデータで構築され、私達が実際にニューラルネットワークを訓練させるために使うものです。

テストデータセットは残りの10%のデータで構築されます。これらの画像はネットワークが正しく機能しているかを確かめるのに使用されます。ニューラルネットワークで、知っている画像に関しては良いパフォーマンスを示し、一度も見たことのない画像に関しては悪いパフォーマンスを示すという事が多くあります。これは *過剰適合* と呼ばれています。いくらかの画像を意図的に訓練データに含めないことによって、より現実的なネットワークのパフォーマンスを引き出すことが可能になります。また、これは違うメソッドをいくつか比べてみる時の基準にもなります。他のデータローダーを試してみるには、上のコードをコピーして `train=False` とします。

```
    test_loader = torch.utils.data.DataLoader(
        datasets.MNIST('../data', train=False, download=True,
                       transform=transforms.Compose([
                           transforms.ToTensor(),
                           transforms.Normalize((0.1307,), (0.3081,))
                       ])),
        batch_size=1000, shuffle=True)
```


## モデルの作成

Pytorchは予め用意されたニューラルネットワーク構造、もしくは自分で定義したものから選んで使用することができます。このチュートリアルでは、自分で軽量なモデルを構築しそれを使ってみることにしましょう。

自分のモデルを構築する場合、PyTorchでは独自のルールに従う必要があります。ネットワーク構造は `torch.nn.Module module` から継承されたクラスとして定義される必要があります。このクラスはネットワークのアウトプットを計算する `foward()` メソッド実装する必要があります。以上の事を踏まえると、ネットワーク構造のコード概観は次のようになります。

```
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()

    def forward(self, x):
        return x
```

このプロジェクトでは、比較的計算能力の低いハードウェアでも訓練出来るように、シンプルな構造を使用することにします。2つの畳み込み層を、続いて2つの全結合層を追加します。独自の構造を思いつくことは科学というよりも芸術に近いものです。実際のケースでは、予め用意されたネットワーク構造を使用する方が良いかもしれません。

これらのレイヤー（層）を `__init__` メソッドの中に定義し、続いてそのレイヤーを `forward()` メソッドの中で呼び出します。

```
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)
```

このブロックは私達のネットワーク内でレイヤーが何になるのかを定義しています。見て分かるように、2つの畳み込み層に続き2つの全結合層が続いています。それでは、さっそく計算させてみることにしましょう:

```
    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2(x), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

```

始めにある2つの計算は畳み込みに関してのブロックです。`self.conv1` を計算、 `F.max_pool2d` を使い max pooling (画像の特徴を表面化させる方法)を実行、そして最後に `F.relu` を使いReLuアクティベーション関数を追加しています。

次のステップでは、ネットワークの出力を '整形' し、その出力を `self.fc1` と共に完全結合層を通過させ、最後にsoftmaxアクティベーションを行います。おめでとうございます、これで畳み込みニューラルネットワークの完成です!

## トレーニング

データと独自ニューラルネットワークの準備が整いました。それでは、さっそく訓練させてみましょう。

他のディープラーニングフレームワークと比べて、PyTorchでのこのステップは笑ってしまうくらい簡単です。

1. 最適化ルーチンを設定しましょう。例として、確率的勾配降下法は `optim.SGD` により調整されます。
2. `model.train()` を使い、モデルの訓練モードを設定しましょう。
3. 最適化ルーチンと訓練のためのデータを準備しましょう。
4. `model(data)` を使い、フォワード・パスを行いましょう。
5. 誤差を計算しましょう。
6. `loss.backward()` を使い、逆伝播を計算しましょう。
7. 次のステップに進みましょう（何か出力してみると良いかもしれません）

この全てのプロセスは文字通りたった10行以下のコードで実装されます。それぞれの行で何が行われているかについてはコメントを見てみてください。

```
device = torch.device("cpu")  # マシンのCPUを使用する
model = Net().to(device)
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)   # 最適化ルーチンの作成
model.train()     # 訓練モードの設定 
for batch_idx, (data, target) in enumerate(train_loader): # 分割データの繰り返し処理
    data, target = data.to(device), target.to(device)  # データをターゲットデバイスに移動
    optimizer.zero_grad()  # 勾配を0に設定
    output = model(data)   # フォワード・パス 
    loss = F.nll_loss(output, target)   # 誤差計算
    loss.backward()  # 逆伝播計算
    optimizer.step()   # 次ステップへの準備
```

## 評価

時には、自分で用意したモデルのパフォーマンスを先程作成したテストデータを使って調べてみたい場合があるかもしれません。これを実現するためには、それぞれのエポック(訓練用データを使う回数の数だけ)の後で、評価ステップを実行します。したがって、先程定義した訓練のためのコードを新しい `train()` 関数の中に定義し直し、以下の事を実行するために新しく `test()` 関数を作ります。

1. `model.eval()` を使い、モデルを評価モードに設定する 
2. テストデータを準備する 
3. フォワード・パスを実行する 
4. 成功数を確認する（出力と実際の数字を比べながら）

これらの事をするためのコードは次のようになります。再びになりますが、それぞれのラインで何が行われているかはコメントを参照してください。

```
model.eval()    # 評価モード
correct = 0    # 予測成功数のカウント
with torch.no_grad():     # 勾配計算の無効化
    for data, target in test_loader:    # テストデータに対する繰り返し処理
        data, target = data.to(device), target.to(device)  # データをターゲットデバイスへ移動
        output = model(data)   # フォワード・パス
        pred = output.max(1, keepdim=True)[1] # 最も高い確率のインデックス取得
        correct += pred.eq(target.view_as(pred)).sum().item()    # 予測が正しければカウント
```

ここまでくれば、ネットワークの訓練とデータ評価を実行する準備は完了です！

## 実行

次に、実際にテスト結果の確認と訓練を実行するために、ちょっとした出力をループの最後に加えてみましょう。

このステップでは、誤差が減少し正答率が増加傾向にあることを確認することが目標です。それぞれの訓練エポックの最後に、訓練の結果が表示されます。10回のエポック後、つまり数分に満たないうちに、次のような結果を確認することが出来ます。

```
$ python train.py
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz
Processing...
Done!
Train Epoch: 1 [0/60000 (0%)]   Loss: 2.302968
Train Epoch: 1 [640/60000 (1%)] Loss: 2.276404
Train Epoch: 1 [1280/60000 (2%)]    Loss: 2.274325
Train Epoch: 1 [1920/60000 (3%)]    Loss: 2.247588
Train Epoch: 1 [2560/60000 (4%)]    Loss: 2.172908
Train Epoch: 1 [3200/60000 (5%)]    Loss: 2.121487
Train Epoch: 1 [3840/60000 (6%)]    Loss: 2.048568
Train Epoch: 1 [4480/60000 (7%)]    Loss: 1.845532
Train Epoch: 1 [5120/60000 (9%)]    Loss: 1.634708
.
.
.(省略)
.
Train Epoch: 10 [56960/60000 (95%)] Loss: 0.006783
Train Epoch: 10 [57600/60000 (96%)] Loss: 0.004069
Train Epoch: 10 [58240/60000 (97%)] Loss: 0.001738
Train Epoch: 10 [58880/60000 (98%)] Loss: 0.012348
Train Epoch: 10 [59520/60000 (99%)] Loss: 0.031503

Test set: Accuracy: 99%
```

おめでとうございます! 完全な0から、99%以上の成功率を誇る手書き認識スクリプトを作成することが出来ました！
これは、10年前の優れた研究者達が可能であったことに比べても、遥かに良いテスト結果であり、しかも数行のコードでそれを実現する事が出来ました！

## 次のステップ

それでは、次のステップでは何をしたら良いでしょうか。さらに100%に近い結果を取得するためには、もしくは 0-9 ではなく 0-100 をネットワークが認識出来るように拡張するためには、どうしたらこれらの限界を越えていけるでしょうか。

- 更に大きな力を付与するために、劇的にネットワークの規模を大きくしてみることも出来ます。
- ドロップアウト（Dropout）という技術を使うことで更にネットワークを一般化し、過剰適合率を減らすことが出来るかもしれません。
- データセットに工夫を加えてみることもいいでしょう。このネットワークは逆さまに配置されている数字を認識することは出来るでしょうか?

可能性は無限大です。ここまでくれば、あなたはもう便利なビギナーツールキットを既に取得したことになります。ここから一歩踏み出して、何か自分で自由に作ってみましょう！