Kerasでディープラーニングに入門する（MNISTの手書き数字問題を単層パーセプトロンで解く）

###前置き###
2年ほど前から仕事で機械学習や多変量解析に携わるようになりました。
最近はディープラーニングが中心で、AI志向にシフトしてきました。
PythonでKeras＋Tensorflow、あるいはscikit-learnで書いてます。

今後、メモ書きレベルかも知れませんが、記事を投稿していきたいと思います。

今回はMNISTの問題をシンプルな単層パーセプトロンで解きたいと思います。
単層パーセプトロンの出番は今やほとんど無い気がしますが、
これからニューラルネットワークを始める人の良い入口になればいいかなと。

###実行環境###
```
言語：Python 3.6
ライブラリ：Keras
プラットフォーム：Google Colaboratory
```
Google Colaboratoryだとクラウド上の仮想マシンでJupyter Notebook形式でPythonを実行できます。
GPUも使えて、無償なので、この分野の勉強には非常に重宝します。

###ソースコードと解説###
######ライブラリのインポート#######
```python:
import numpy as np
from keras.models import Sequential
from keras.layers import Dense
from keras.utils import to_categorical
from keras.datasets import mnist
```

単層パーセプトロンは入力層と出力層しか持ちませんので、
Sequentialモデルで全結合層のDenseだけで良いです。
あとはユーティリティーとしてのNumpyとOne-Hotベクトル化するto_categoricalがあれば、十分です。

######単層パーセプトロンのモデル定義#######
```python
def single_layer_perceptron():
  ret = Sequential()
  ret.add(Dense(10, input_dim=28*28, activation='softmax'))
  ret.compile(optimizer='sgd', loss='categorical_crossentropy')
  return ret
```
モデルはこれだけです。
入力次元は`28×28`で、出力は0～9までの10カテゴリなので`10`次元になっている全結合層(`Dense`)で出力を`softmax`関数を通します。
モデルのコンパイル時に最適化関数と損失関数を指定します。
損失関数の値を小さくするように重みを更新するアルゴリズムが最適化関数です。

今回は多クラス分類なので、損失関数は`categorical_crossentropy`、最適化関数はパーセプトロンなので`sgd`としました。

あとGitHubなんかにアップされているサンプルプログラムだとモデルは別ソースのクラスで定義しているケースが多いですが、`オブジェクト指向`が理解できていないとちょっと難しくなるので、ユーザー定義関数内でモデルを作りコンパイルまでしてから実体を戻り値としています。

######データの準備#######
```python
if __name__ == '__main__':
  (x_train, y_train), (x_test, y_test) = mnist.load_data()
  x_train = (x_train.astype(np.float) / 255.).reshape(-1, 28*28)
  y_train = to_categorical(y_train)
  x_test = (x_test / 255.).reshape(-1, 28*28)
  y_test = to_categorical(y_test)
  
  print(x_train.shape, y_train.shape)
```
Keras付属のMNISTデータセットを読み込みます。
※初回はインターネットからのダウンロードが発生します。

説明変数は画像の形で入ってくるので、1データが1行になるように`reshape`し、255で割ることで画素値を0～1にスケールダウンします。
目的変数は`to_categorical`でOne-Hotベクトルに変換します。
One-Hotベクトルについてはまた今度。
最後の`print`による標準出力は下記のようになります。

```python:output
(60000, 784) (60000, 10)
```

######モデルの学習#######
```python
  slp = single_layer_perceptron()
  slp.fit(x_train, y_train, batch_size=1024, epochs=50, validation_split=0.25)
```
関数を呼び出してモデルのインスタンスを受け取り、`fit`で学習します。
`fit`の第1引数が説明変数で、第2引数が目的変数です。
バッチサイズとエポック数を指定します。省略できますが、ハイパーパラメータなので、指定するのが一般的です。
バッチサイズが一度に投入するデータ量になり、GPUによる並列計算を行う数になりますが、重みの更新がこの数毎にしか行われません。
大きいと外れ値に影響されにくくなりますが、最適解にたどり着かない可能性があり、小さいと局所解に陥った後抜け出せなくなります。
エポック数は反復学習する回数で、これも適切な回数にしないと学習が十分でなかったり、やりすぎると過学習を起こします。

さて、学習が始まるとエポック毎の損失関数の値が標準出力されていきます。

```python:output
Train on 45000 samples, validate on 15000 samples
Epoch 1/50
45000/45000 [==============================] - 1s 17us/step - loss: 2.2434 - val_loss: 2.0094
Epoch 2/50
45000/45000 [==============================] - 1s 13us/step - loss: 1.8627 - val_loss: 1.6877
Epoch 3/50
45000/45000 [==============================] - 1s 13us/step - loss: 1.5916 - val_loss: 1.4573
--- (中略) ---
Epoch 47/50
45000/45000 [==============================] - 1s 14us/step - loss: 0.4934 - val_loss: 0.4704
Epoch 48/50
45000/45000 [==============================] - 1s 14us/step - loss: 0.4904 - val_loss: 0.4677
Epoch 49/50
45000/45000 [==============================] - 1s 14us/step - loss: 0.4876 - val_loss: 0.4651
Epoch 50/50
45000/45000 [==============================] - 1s 14us/step - loss: 0.4848 - val_loss: 0.4625
```
まぁ、単層パーセプトロンなので、こんなものかと。
もう少し下がりそうな気もしますが・・・・
lossは学習データ、val_lossは`validation_split`で指定した下方25%分の検証データでの損失関数値です。

最後にテストデータを使って評価します。

```python
slp.evaluate(x_test, y_test)
```
```python:output
10000/10000 [==============================] - 0s 20us/step
0.4556954589366913
```
学習時の検証データと大体同じですね。

###最後に###
今回は単層パーセプトロンでMNISTに取り組みましたが、単層パーセプトロンは線形問題しか解けません。
MNISTは画像分類の問題なので、無理気味なことをしています。
多層パーセプトロンやCNNにするともっと低いlossを出せます。
※入門と言っても多層パーセプトロンでも良かったかも・・・

もっと言ってしまうと単層パーセプトロンはscikit-learnにもあります。
>[sklearn.linear_model.Perceptron](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Perceptron.html)

今回はこれから始める人の入口として、敢えてKerasで実装しました。
有識者の方には間違っていたり、変な事を言っていたら、ご指摘いただければと思います。