Kerasで機械学習に初挑戦した記録

バックエンド、フロントエンドをいろいろカジってきましたが、機械学習は未挑戦でした。今般、初挑戦しましたので記念に記録しておきます。python、numpy、tf.kerasを使っています。

マイスペック

+ このQiita参照。
+ cやgoによる通信系〜バックエンド開発、flutter/dartによるフロントエンド開発が可能。
+ pythonも結構触っている。
+ 機械学習はGUIのツールでちょっと触ってみたことはある。
+ pythonでの機械学習は初挑戦。numpyも使ったことが無かったレベル。

機械学習の理論をまとめて勉強するために「[ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装](https://www.amazon.co.jp/
dp/4873117585/)」を読みました。とても良い本でした。

開発環境はPyCharm Community 2019.3です。Anacondaとかは使わずにPyCharmに必要なライブラリを読み込ませて使っています。

#1. 機械学習の課題設定

以下の正解ロジックを、機械学習することを目指します。

+ 教師ありの２値分類問題とします。
+ 入力する特徴量を2つの乱数値（0以上1未満）として、2つの大小比較によって0か1を正解ラベルとします。
+ 正解ラベルには稀にノイズを混入させます。（最初はノイズを無しにします）

#2. コード

２値分類問題の典型的コードを、いくつかのWeb記事を見ながら作ってみました。結構、コンパクトに直感的に記述できるものだと思いました。Kerasすごい。

```python
#!/usr/bin/env python3

import tensorflow as tf
import numpy as np
from tensorflow.keras.metrics import binary_accuracy
import matplotlib.pyplot as plt

# データセット準備
ds_features = np.random.rand(10000, 2)  # 特徴データ
NOISE_RATE = 0
ds_noise = (np.random.rand(10000) > NOISE_RATE).astype(np.int) * 2 - 1  # noiseなし: 1, あり: -1
ds_labels = (np.sign(ds_features[:, 0] - ds_features[:, 1]) * ds_noise + 1) / 2  # 正解ラベル

# データセットを訓練用と検証用に分割
SPLIT_RATE = 0.8   # 分割比率
training_features, validation_features = np.split(ds_features, [int(len(ds_features) * SPLIT_RATE)])
training_labels, validation_labels = np.split(ds_labels, [int(len(ds_labels) * SPLIT_RATE)])

# モデル準備
INPUT_FEATURES = ds_features.shape[1]   # 特徴量の次元
LAYER1_NEURONS = int(INPUT_FEATURES * 1.2 + 1)   # 入力次元より少し広げる
LAYER2_NEURONS = LAYER1_NEURONS
LAYER3_NEURONS = LAYER1_NEURONS  # 隠れ層は3層
OUTPUT_RESULTS = 1  # 出力は一次元
ACTIVATION = 'tanh'
model = tf.keras.models.Sequential([
    tf.keras.layers.Dense(input_shape=(INPUT_FEATURES,), units=LAYER1_NEURONS, activation=ACTIVATION),
    tf.keras.layers.Dense(units=LAYER2_NEURONS, activation=ACTIVATION),
    tf.keras.layers.Dense(units=LAYER3_NEURONS, activation=ACTIVATION),
    tf.keras.layers.Dense(units=OUTPUT_RESULTS, activation='sigmoid'),
])
LOSS = 'binary_crossentropy'
OPTIMIZER = tf.keras.optimizers.Adam   # 典型的な最適化手法
LEARNING_RATE = 0.03   # 学習係数のよくある初期値
model.compile(optimizer=OPTIMIZER(lr=LEARNING_RATE), loss=LOSS, metrics=[binary_accuracy])

# 学習
BATCH_SIZE = 30
EPOCHS = 100
result = model.fit(x=training_features, y=training_labels,
                   validation_data=(validation_features, validation_labels),
                   batch_size=BATCH_SIZE, epochs=EPOCHS, verbose=1)

# 表示
plt.plot(range(1, EPOCHS+1), result.history['binary_accuracy'], label="training")
plt.plot(range(1, EPOCHS+1), result.history['val_binary_accuracy'], label="validation")
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.ylim(0.5, 1)
plt.legend()
plt.show()

```

#3. 結果

こちかが学習結果です。おおよそ99%くらいの精度にすぐに到達し、過学習もしていないようです。

![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156600/ad94fdab-eaec-10e2-5f76-65ca42f81217.png)


#4. 考察
##4.1. ノイズを加えた時の挙動

NOISE_RATE = 0.2 としてみました。ノイズ分だけ精度が低くなりますが、適切な結果に到達しています。

![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156600/06318813-2052-2ad9-61c1-fcadbbd816fe.png)



##4.2. 無関係なダミーの特徴量を加えた時の挙動

ノイズを戻し、特徴量を5種に増やしてみます。5種のうち2種だけ使って同じロジックで正解ラベルを求めます。すなわち、特徴量の残り3種は、正解とは全く関係無いダミーとなります。

結果はこちらで、多少ブレ幅が大きくなりますが、ダミーに騙されずに学習できているといえます。

![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156600/0f5c2290-89c1-2ac0-60cc-f9e433271034.png)

##4.3. 特徴量の正規化を崩した時の挙動

特徴量を2種に戻しますが、0以上1未満の乱数値を×1000してみました。結果は、学習が一律に収束していかないように見えるほか、最終エポックの近くで精度が悪化しています。

![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156600/ba82089f-8d5f-fa78-8793-bb836c4977dc.png)

エポックを増やして確認してみました。やはり学習が安定していないようです。

![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156600/d3acb4e0-742b-6467-d275-28c7975cbfdb.png)


一方、特徴量の平均をずらし、0以上1未満の乱数値を+1000してみました。結果は、精度がほぼ0.5、すなわち2値分類としては全く学習されないことがわかりました。

![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156600/c85bffb3-9b40-521c-e1cc-883b6a6099be.png)


全体的に、特徴量の正規化が大切であることがわかります。