PyTorchでシンプルな多層ニューラルネットワークを作ろう

多層ニューラルネットワークのHello Worldとして、深層学習ライブラリのPyTorchを使ってアヤメのデータを分類してみましょう。実際に自分の手で多層ニューラルネットワークを作り、深層学習の基本的な構造が体験できます。深層学習には他の機械学習とはまた違った奥深さがあります。ぜひ、エキサイティングなコーディング時間を楽しみましょう！

##前提環境
簡便のため環境はクラウドを利用します。ここではAmazon Web ServicesのDeep Learning AMIを利用し、関連パッケージがプリインストールされた状態でインスタンス作成ができ便利です。また、OSはUbuntu、Deep Learning AMIのバージョンは5を利用します。なお、GPUインスタンスはp2.xlargeを選択しました。インスタンスを起動させてSSHでサーバに接続したら、まずPythonの環境をPyTorch用に切り替えましょう。

```Bash
$ source activate pytorch_p36
```

それでは、pythonを起動し、PyTorchが利用できるか確かめてみましょう。

```Bash
$ python -i
>>> import torch
>>>
```
エラーが出力されなければPyTorchが利用できる状態です。

##データセット
機械学習のサンプルデータセットとしてよく用いられる、フィッシャーのアヤメデータを使います。サンプル数は150あり、4つの実数からなる特徴量と、アヤメの種類に対応する正解インデックスです。Pythonの機械学習ライブラリであるscikit-learnを利用すると、簡単にデータセットを読み込みことができます。

```python
from sklearn import datasets

...

iris = datasets.load_iris()
```

前処理として、正解ラベルをone-hot-vector表現のベクトルに変換します。

```python
y = np.zeros((len(iris.target), 1 + iris.target.max()), dtype=int)
y[np.arange(len(iris.target)), iris.target] = 1
```

次に、教師データの25%をテストデータとし、予測精度の評価のためにとっておきます。残りを訓練データとして使います。慣例に従い、Xを特徴量、yを正解として表します。

```python
from sklearn.model_selection import train_test_split

...

X_train, X_test, y_train, y_test = train_test_split(iris.data, y, test_size=0.25)
```

また、これらをPyTorchで取り扱える形式に変換します。

```python
x = Variable(torch.from_numpy(X_train).float(), requires_grad=True)
y = Variable(torch.from_numpy(y_train).float())
```

##ネットワーク定義
ここでは、シンプルな多層ニューラルネットワークとして、2つの隠れ層を持つネットワークを作ります。概念図は以下です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/242602/0dbd9ae9-90ae-5090-f8a3-3745f7e0a433.png" width="50%">

ここで、上図のfc1は入力に重みとバイアスによる線形変換を行う関数であり、これにより隠れ層を定義していると理解できます。入力と出力が全結合(fully connected)であるためfc1としています。また、fc2、fc3も同様です。これらの関数は順伝播の際にforwardメソッド内で実行され、活性化関数のReLU関数に与えられます。

```python
class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(4, 10)
        self.fc2 = nn.Linear(10, 8)
        self.fc3 = nn.Linear(8, 3)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
```

##学習
実際の学習に際して、以下のコードによりネットワークのインスタンス化、パラメータ更新手法の指定、学習率の指定、コスト関数の指定を行います。

```python
net = Net()
optimizer = optim.SGD(net.parameters(), lr=0.01)
criterion = nn.MSELoss()
```

そして、実際の学習として3000エポックのループを回し、パラメータを更新していきます。

```python
for i in range(3000):
    optimizer.zero_grad()
    output = net(x)
    loss = criterion(output, y)
    loss.backward()
    optimizer.step()
```

##評価
学習の評価には、冒頭に退避しておいたテスト用データを使うことができます。ここでは、ネットワークは確率分布のような実数ベクトルが出力されるので、最大値のインデックスと正解のインデックスを比較することで、予測が正しいかがわかります。したがって、正解個数をテストデータ数で割ることで予測精度を計算することができます。

```python
outputs = net(Variable(torch.from_numpy(X_test).float()))
_, predicted = torch.max(outputs.data, 1)
y_predicted = predicted.numpy()
y_true = np.argmax(y_test, axis=1)
accuracy = (int)(100 * np.sum(y_predicted == y_true) / len(y_predicted))
print('accuracy: {0}%'.format(accuracy))
```

以下が実行結果です。

```Bash
$ python -i iris-net.py
accuracy: 97%
>>>
```

また、架空の未知のデータ(5.6, 4.3, 1.5, 0.35)に対する予測ラベルを計算してみましょう。

```Bash
>>> predict((5.6, 4.3, 1.5, 0.35))
0
```

##ソースコード

```python:iris-net.py


import torch
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split


# network definition
class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(4, 10)
        self.fc2 = nn.Linear(10, 8)
        self.fc3 = nn.Linear(8, 3)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

iris = datasets.load_iris()
y = np.zeros((len(iris.target), 1 + iris.target.max()), dtype=int)
y[np.arange(len(iris.target)), iris.target] = 1
X_train, X_test, y_train, y_test = train_test_split(iris.data, y, test_size=0.25)
x = Variable(torch.from_numpy(X_train).float(), requires_grad=True)
y = Variable(torch.from_numpy(y_train).float())
net = Net()
optimizer = optim.SGD(net.parameters(), lr=0.01)
criterion = nn.MSELoss()

for i in range(3000):
    optimizer.zero_grad()
    output = net(x)
    loss = criterion(output, y)
    loss.backward()
    optimizer.step()

# test
outputs = net(Variable(torch.from_numpy(X_test).float()))
_, predicted = torch.max(outputs.data, 1)
y_predicted = predicted.numpy()
y_true = np.argmax(y_test, axis=1)
accuracy = (int)(100 * np.sum(y_predicted == y_true) / len(y_predicted))
print('accuracy: {0}%'.format(accuracy))


# utility function to predict for an unknown data
def predict(X):
    X = Variable(torch.from_numpy(np.array(X)).float())
    outputs = net(X)
    return np.argmax(outputs.data.numpy())
```

##まとめ
ここでは、多層ニューラルネットワークのHello Worldとして、シンプルなネットワーク構造による深層学習を実装しました。応用として、コンピュータビジョンや自然言語処理などの実タスクでは、畳み込みネットワーク、LSTMやGANなど、より複雑なモデルが利用されます。しかし、ベースとしている誤差逆伝播法やパラメータ更新などは同じです。ぜひ次のステップとして、手書き数字の認識などにチャレンジしてみてはいかがでしょうか。