機械学習の復習

## はじめに
この記事は機械学習の勉強を始めた私自身のためのメモです。
ここに書かれている内容は Keras 及び Tensorflow で機械学習を行う際に知っておくべきことを記しています。

## 機械学習の種類
#### 教師あり学習
・入力値と目的値のサンプルデータセットを用いて入力データを目的値にマッピングするための学習。サンプルデータは人の手でアノテートされており、目的値はアノテーションとも呼ばれる。


| 教師あり学習       |       説明   |
|:-----------------|:--------------------------------|
| 分類           |入力データからラベルを予測する|
| 回帰           |連続値を予測する|
| シーケンス生成  | 与えられた画像のキャプションを予測する|
| 構文木予測      |与えられた文章から構文木への分解を予測する|
| 物体検出        |分類問題や同時分類/回帰問題として表現されることがある|
| 画像分割        |与えられた画像の特定のオブジェクトをピクセルレベルでマスクする|


#### 教師なし学習
・目的値の助けを借りずに入力データの重要な変換を見つけ出す。
データの可視化、データの圧縮、データのノイズ除去目的で利用されたり、データの相関関係の分析で利用されたりすることがある。

主な教師なし学習
・次元削減
・クラスタリング

#### 自己学習
教師あり学習～自己学習～教師なし学習は境界は非常にあいまいであり、学習メカニズムで分類するか、適用範囲で分類するかによって自己学習に分類されているものでも、教師あり学習と捉えられることもあれば、教師なし学習と解釈されることもある。

・アノテーションラベルのない教師あり学習（教師あり学習の特別版）。
・未来の入力データを教師として学習する。


主な自己学習
・オートエンコーダー
・動画の過去のフレームに基づいて次のフレームを予測する、文章から次のフレーズを予測する。


#### 強化学習
・ある環境下で何らかの報酬が最大になるような行動の選び方を学習する。
alphaGoが有名。

## 機械学習のモデルの評価方法
- モデルの訓練、検証、テスト実施時にはデータに偏りが生じないようにシャッフルするのが望ましいが、天気予報や株価予想など過去に基づいて未来を予測する場合はデータをシャッフルしてはならない。
- 重複したデータがある場合もシャッフルしてしまうと、訓練データでテストデータを行う可能性があるため、テストの精度が保証できないのでNG。

### ホールドアウト法（hold out validation)
データを訓練用データ、検証用データ、テストデータにわけておき、モデルのチューニング、検証、テストを各データで行うやり方

### K分割交差検証
データをK個のサブセット（フォールド）に分割し、n番目のフォールドごとにn番目のフォールドで検証、残りのフォールドでは訓練をする、という作業をk回繰り返す。最終的に、K個のスコアの平均で評価する。

| 分割数       　　　|1回目|2回目|3回目|4回目|5回目|スコア  |
|:-----------------|----|------|------|---|----:|-------|
| フォールド1       |検証 |訓練 |訓練 |訓練 |訓練 |検証結果1|
| フォールド2       |訓練 |検証 |訓練 |訓練 |訓練 |検証結果2|
| フォールド3       |訓練 |訓練 |検証 |訓練 |訓練 |検証結果3|
| フォールド4       |訓練 |訓練 |訓練 |検証 |訓練 |検証結果4|
| フォールド5       |訓練 |訓練 |訓練 |訓練 |検証 |検証結果5|

#### K分割交差検証の例

``` sample.py
# データをシャッフル
np.random.shuffle(data)
# 検証結果用配列
validation_scores = []

k = 5
num_validation_samples = len(data) // k

# データをK個に分割をn回繰り返す
for n in range(k):
  fold_array_start = num_validation_samples * n
  fold_array_end   = num_validation_samples * (n + 1)
  # n番目のデータを検証データにセット
  validation_data = data[fold_array_start : fold_array_end]
  # 残りのデータは訓練データに使用する ※訓練データ = 検証データの前後のデータ
  train_data = data[:fold_array_start] + data[fold_array_end:]
  # モデルのインスタンスを生成
  model = get_model()
  # モデルの訓練
  model.train(train_data)
  # 検証スコアを取得
  validation_score = model.evaluate(validation_data)
  validation_scores.append(validation_score)

validation_score = np.average(validation_scores)
# テストにまったく使用していないデータで最終的なモデルを訓練
model = get_model()
model.train(data)
# テストスコア
test_score = model.evaluate(test_data)
```

## データの前処理
- データの前処理は生のデータをニューラルネットワークモデルに渡すためのデータの型に整形する作業のことで**ベクトル化、正規化、欠測値の処理、特徴エンジニアリング**がある。

#### ベクトル化
ニューラルネットワークの入力値と目的値はflow型のテンソルである必要がある（整数値の場合もあり）。
one-hotエンコ―ディングした上でfloat32型に変換するケースが多い。

### 正規化
入力値ごとに特徴量の範囲が異なる場合、（例えば、株価の予想問題で日経平均株価とTOPXの値を入力値に持つ等）正規化することでニューラルネットワーク側で入力値を均等に扱えるようになる。
大きな値をもつデータや種類の異なる値をとるデータをニューラルネットワークに渡すのは安全ではない。
正規化の手法としては標準偏差:1、平均値:0になるようにすることが多い。
ネットワークの学習を容易にするには、1.小さな値をとる(0～1の範囲の値をとるようにする)、2.種類が同じ（つまり、全ての特徴量が0~1の範囲の値をとるようにする）ことが望ましい。

``` sample.py
# x は (smples, features)の2次元配列
# axis=0は各配列の1軸目（各列ごとの演算を行う）
x -= x.mean(axis=0)
x /= x.std(axis=0)
```

### 欠測値(missing value)の処理
特徴量によっては存在しないデータが含まれている場合がある。
そうしたデータの扱い方（ルールの定義）を決めるのが欠測値の処理である。
一般的には0にするのが無難であるが、0が意味のある値として使用されている場合は欠損値としては不適格である。
また、テストデータに欠測値があるが、訓練データには欠測値がない場合もモデルは欠測値を学習していないので正しい目的値にはたどり着けない。こうした場合は人工的に欠測値を作成する必要がある。

### 特徴エンジニアリング
生のデータセットをモデルに最適化したデータに変換する作業のこと
例えば時刻のデータの場合、時計の画像（生データ）から、1.時計の針の座標軸、2.時計の針の角度へと最適化させること。

## 学習不足と過学習
機械学習のゴールはモデルの最適化と汎化の間のどこかにある。最適化はモデルの訓練を経てモデルが訓練を経て予測値が目的値により近づくことであり、汎化は訓練済みモデルが未知のデータに対して予測値をより目的値へ近づけること。
データと目的値のセットで機械学習をしていくことでモデルはデータと目的値の関係を学習していく。
同じデータで何回も訓練を行へば訓練データに対する最適化が進むが、訓練データによる学習が一定を超えると訓練データに特化してしまい、未知のデータでは損失値が大きくなる。これを過学習と呼ぶ。
過学習を回避する最も良い方法は訓練データの数を増やすこと。
次善策はモデルに格納できる情報量を調整すること（ネットワークを小さくすること）。
過学習を克服するプロセスを正則化(regularization)と呼ぶ。

#### ネットワークサイズの調整
過学習を回避する方法の一つがモデルのサイズを小さくすること。
つまり、パラメータの数を減らすこと。
モデルのパラメータとは層の数とユニット数（入力数≒深度）によって決まる。
モデルの積雪なサイズを割り出す為の一般的なワークフローは少ない数の層とパラメータから徐々に層の数とパラメータの数を大きくしていき、検証データセットでの損失値が収穫遁減が現れるところまで実施していく。

### 重みの正則化
ネットワークの重みに小さい値だけを設定することで、パラメータ値のエントロピーの増大を抑制させ、値の分布を正則化する手法。ネットワークの損失関数にコストを追加することで実装する。
コストのは次の2種類がある。  
L1 正則化：追加されるコストは重み係数の絶対値（L1ノルム）に比例 
L2 正則化：追加されるコストは重み係数の値の二乗（L2ノルム）に比例※荷重減衰(weight decay)とも呼ぶ

```py:sample.py-例：モデルにL2正則化を追加-
from keras import regularizers

model = models.Sequential()
model.add(
  layers.Dense(
    16,
    kernel_regularizer=regularizers.l2(0.001), 
    activation='relu',
    input_shape=(10000,)
  )
)
model.add(
  layers.Dense(
    16,
    kernel_regularizer=regularizers.l2(0.001), 
    activation='relu'
  )
)
model.add(layers.Dense(1, activation='sigmoid'))

# L1 正則化関数
regularizers.l1(0.001)
# L2 正則化関数
regularizers.l2(0.001)
# L1, L2 正則化同時使用
regularizers.l1_l2(l1=0.001, l2=0.001)
```

### ドロップアウト(dropout)を追加
ニューラルネットワークにおいて最も効果的で最も使用されている正則化手法がドロップアウト。
**ドロップアウトは訓練中のレイヤーの出力特徴量の一部がランダムで選択されるとそれらの値が0にセットされる。**
例えば、特徴量が次のような[0.2, 0.5, 0.5, 0.9, 0.6]を返すレイヤーがあった場合、ドロップアウトによって[0.2, 0, 0.5, 0, 0.6]のように値の一部が0にセットされるようになる。
ドロップアウト率(dropout rate)はドロップアウトする特徴量の割合のことで0.2～0.5の間で設定されることが多い。ドロップアウトが適用されるのは訓練時のみでテスト時には適用されないかわりに出力値がドロップアウト率と同じ割合でスケールダウンされる。

```py:sample.py-例：モデルにドロップアウト追加-
model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))
```

### データ拡張（水増し）戦略
訓練データを様々な手法を用いてデータを加工することでデータの数を増やして訓練させる戦略。
（画像系の分類問題でデータが少ない場合に用いる）
訓練データが少ないときにデータ拡張を用い、訓練データを増やすことで過学習を抑制させる効果が期待できる。
画像の場合は画像を回転させたり、反転させたり、トリミング位置を移動させたり、拡大・縮小させたりするなどの加工処理を施すことで実際にデータを水増しさせる。

```py:sample.py-例：データ拡張（水増し）戦略-
from keras.preprocessing import image
import matplotlib.pyplot as plt
from keras.preprocessing.image import ImageDataGenerator

# データ拡張の実践例
datagen = ImageDataGenerator(
    rotation_range      = 40,       # 画像をランダムに回転させる範囲(0 - 180)
    width_shift_range   = 0.2,      # 切り取り位置をランダムに移動させる範囲
    height_shift_range  = 0.2,      # 切り取り位置をランダムに移動させる範囲
    shear_range         = 0.2,      # 等積変形をランダムに適用
    zoom_range          = 0.2,      # 描画内をランダムにズーム
    horizontal_flip     = True,     # 画像の半分を水平線方向にランダムに反転
    fill_mode           = 'nearest' # 描画範囲がオリジナルデータ外の場合の描画方法
)

# 画像パスを取得
fnames = [os.path.join(train_cats_dir, fname) for fname in os.listdir(train_cats_dir)]

# 加工する画像を選択
img_path = fnames[3]
# 画像を150*150で読み込む
img = image.load_img(img_path, target_size=(150, 150))
# (150, 150, 3)のnumpy配列に変換 
x = image.img_to_array(img)
# (1, 150, 150, 3)のテンソルに変換
x = x.reshape((1,) + x.shape)


# flow() : 指定したファイルを生成する関数 batch_size = 処理単位
# 水増しデータをi 個作成
i = 4
for batch in datagen.flow(x, batch_size = 1):
    plt.figure(i)
    imgplot = plt.imshow(image.array_to_img(batch[0]))
    i -= 1
    if i <= 0:
        break

plt.show()
```
![ダウンロード (1).png](https://qiita-image-store.s3.amazonaws.com/0/294552/f13d2594-78e3-008b-30fb-c58a7c262611.png)
![ダウンロード (2).png](https://qiita-image-store.s3.amazonaws.com/0/294552/c316c1f9-a84e-958b-2289-d25dfe027c5a.png)
![ダウンロード (3).png](https://qiita-image-store.s3.amazonaws.com/0/294552/24d767fc-f363-8ab2-0385-34fa83b17865.png)
![ダウンロード (4).png](https://qiita-image-store.s3.amazonaws.com/0/294552/98a52ac3-31ad-d47a-a754-8db975e29128.png)


## まとめ
これまで学んできたことの整理です。
個人的には演習をたくさんしたいのだけれど、時には整理も大事よね。