ゼロから始める機械学習 #3

# 最終目的

プログラミングを全くやったことがないという状態から，機械学習を行えるようになるまで，順に勉強する．

第二回目は，下記参照

https://qiita.com/t-baba-11/items/6dc947e3f3b33485ccf6

# 第三回 目標と流れ

- 目標 : データに対して，機械学習を行い，予測値を出力する．
- 流れ : 
    - 使用するライブラリの説明
        - os
        - glob
        - random
        - numpy
        - TensorFlow
        - ライブラリのインポート
    - 学習方法の選択
        - 深層学習とは
    - 評価指標
        - 二乗平均平方根誤差
    - 学習データの前処理
        - 全データの読み込み
        - 欠損データの0埋め
        - 文字列数値化
    - データの正規化
    - 学習モデルの作成
    - テストデータへの適用
    - パラメーターのチューニングについて

プログラムは新規に作成し，記載していく．


# 使用するライブラリの説明

###os

オペレーティングシステムのインタフェース.
tensorflowはGPUの設定を行っていなくても，動作するが，その場合警告が多数表示されるため，
osを使って表示させないように設定している．

###glob

特定のパターンにマッチするファイルを取得するための，Pythonのモジュール．

###random

ランダムな値を出力するためのPythonモジュール．
機械学習では内部でランダムな値を用いるものが多いため，毎回結果が少し変わってしまう，
それを防ぐため，ランダムな値を固定化するために使う．

###numpy

数値計算を高速に行うためのライブラリ.
pandasのデータフレームの方が可視化には優れるが，numpyの方がより高速に動作する．

### TensorFlow

Pythonの機械学習用ライブラリ．
機械学習，数値解析，深層学習に対応している．
また，ニューラルネットワークライブラリである，Kerasが内部に含まれており，TensorFlowをインストールするだけでそちらも使用可能．

### scikit-learn

Pythonの機械学習ライブラリ．
学習モデルの構築や評価に使用する．
サポートベクターマシン、ランダムフォレストなどの様々な機械学習を備えている．
今回は，サンプルプログラムの最小二乗線形回帰にて用いている．



### ライブラリインポート

```.py
import pandas as pd
import glob #ファイル読み込み用
import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2' #GPU環境がない場合に警告を表示しない
import random
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential #モデル定義用
from tensorflow.keras.optimizers import Adam #最適化アルゴリズム
from tensorflow.keras import backend as K #RMSEを定義するのに使用
from tensorflow.keras.layers import Dense, Dropout #Kerasのレイヤーのインポート
os.environ['TF_DETERMINISTIC_OPS'] = '1' #GPUを使ってtensorflowを使う場合は乱数固定のために必要
```

# 学習方法の選択

今回，学習するのは，バッテリーの充電率予測である．
訓練データには，複数のパラメータと充電率が記載されているため，教師あり学習として，解くことができる．
充電率は，連続する値であるため，回帰の問題として，解くことができる．

すなわち，教師あり学習で，回帰の問題を解く．

詳しくはやらないが，回帰の問題を解ける学習法として

 - 深層学習(DNN)
 - サポートベクター回帰(SVR)
 - k近傍法回帰(k-NN回帰)
 - 混合ガウスモデル回帰分析(GMR)
 - ランダムフォレスト
 - XGBoost回帰
 - 回帰木
 - 最小二乗線形回帰
 - リッジ回帰
 - ラッソ回帰

などがある．

学習方法は，データの量や特徴量の数，解くべき目標にあったものを選択しなければならない．

例えば，データ数も多く特徴量の数が1000種類といった数になるような場合は，深層学習を使って特徴量を自動で求めると，ある程度の性能の高いものが得られると考えられる．
逆に，データ数が少なく，相関の非常に高い特徴量がある場合には，単純な線形回帰の方が性能が良くなることもある．

※ 今回は，時間の関係で単純パーセプトロンのみ使用

###深層学習とは

多層で構成されたニューラルネットワーク．
学習方法の中でも特徴量を自動で決めてくれるという特徴がある．
第二回で行った，特徴量の抽出をある程度自動で行ってくれる．


今回の勉強会では，学習法として，基本となる単純パーセプトロンを用いている．

※ 単純パーセプトロン : これ自体は深層学習ではないが，入力層と出力層のみのニューラルネットワークであり，基本となる．線形分離可能な問題しか解くことができない．
※ 深層学習ではない機械学習を使う場合は，特徴量は人間が判断する必要がある．第二回参照．

# 評価指標

機械学習の評価指標は，目的に応じて，決定する必要がある．

今回の充電率予測では，評価指標として，二乗平均平方根誤差が用いられている．

### 二乗平均平方根誤差（RMSE：Root Mean Squared Error）

正解値と予測値の差（＝誤差）を二乗した値を計算し、その総和をデータ数で割った値（＝平均値）の平方根
予測した値が正解に近いほど値が小さくなる．

```math
x = 正解値\\y = 予測値\\
```

```math
RMSE = \sqrt{\frac{1}{n}\sum_{i = 1}^{n}(x_{i} - y_{i})^{2}}
```

例 :
x = [3, 5, 8] #正解値
y = [2, 4, 10] #予測値

```math
RMSE = \sqrt{\frac{1}{3}\{(3-2)^{2} + (5-4)^2 + (8-10)^2}\}
     = \sqrt{2}
```


損失関数と評価指標に使うRMSEの関数定義はないので，自分で定義する．
学習の際に下記の関数を読み込む．

```.py
#RMSEを定義
def root_mean_squared_error(y_true, y_pred):
    return K.sqrt(K.mean(K.square(y_pred - y_true)))

#ランダムシードを設定する関数．学習の直前に使用する．
def set_seed(seed=42):
    tf.random.set_seed(seed)
    np.random.seed(seed)
    random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed) 
```

#学習データの前処理

前回までは，1ファイルのみ読み込んでいたが，実際には，全ファイルを読み込む必要がある．
各ファイルごとにDataFrameを作成し，最後にすべて結合させている．
また，機械学習を行うにあたり，すべてのデータを数値化する必要があるため，
データが欠損している部分の0埋めと，文字列の数値化を行う．

###全データの読み込み

全ファイルの読み込みと結合を行う．

```.py
df_list = []
#全ファイルをDataFrame形式で読み込む
for filename in glob.glob("./data/train/*.csv"):
    print(filename)
    raw_dataset = pd.read_csv(filename)
    df_list.append(raw_dataset.copy())
df = pd.concat(df_list) #結合

#テストデータの読み込み
filename = "./data/test.csv"
test_df = pd.read_csv(filename)
test_df = test_df.drop(["ID"], axis=1) #IDは不要なので，除去
```

###欠損データの0埋め

実際にファイルを確認したところ，値が存在しない部分があったため，
その部分に0を埋めることで，数値化する．

```.py
df = df.fillna(0)
test_df = test_df.fillna(0)
```


###文字列の数値化

今回は，必要ないが知識として記載する．

Drive Cycleのカラムには，"Cycle_1", "dc_NN", "dc_US06"といった文字列が含まれているため，数値化する．

```.py
df["Drive Cycle"]
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189046/f35db53d-8f4a-b8f2-8f9a-ed90f326fa6d.png)

文字列ごとに，その文字列があれば1，なければ0の行列を作る．
これをOne-Hotエンコーディングという．

```.py
dc_df = pd.get_dummies(df["Drive Cycle"], prefix="dc")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189046/210c2ee0-eecb-0055-9864-1fa0d8879dbf.png)


###不要カラムの除去

テストデータに入っていない，"Ah"や"Wh"などの値を除去

```.py
y = df["SOC"] #正解値のデータのみ抽出
X = df.drop(["SOC", "TimeStamp", "Drive Cycle", "Ah", "Wh"], axis=1) #不要カラムの除去
```


# データの正規化

データはカラム毎に単位が違うため，そのままでは，うまく学習できない．
そこで，正規化を行い，各データの幅をそろえる必要がある．


### pandasでの正規化の例

mean()  : 平均を求める関数
std()   : 標準偏差を求める関数


```.py
X = (X - X.mean()) / X.std() #訓練データの正規化
```

#学習モデルの作成

<!--
###訓練データとテストデータの分割

X_train      : 訓練用の予測用データ
X_test       : 訓練用の正解値データ
y_train      : テスト用の予測用データ
y_test       : テスト用の正解値データ
test_size    : テスト用データの割合
ramdom_state : 分割する際の乱数値(指定することで，毎回同じ分割になる)

```.py
X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.1, random_state=42)
```
-->


###レイヤーの設定
レイヤーの追加．今回は基本のDense(全結合)を使用．パラメータをチューニングする際に，レイヤーを追加していく．
(今回は，勉強会なので，入力層と出力層のみの単純パーセプトロンにしている)
ただし，今回は回帰の問題であるため，出力層のレイヤーは必ず1にする必要がある．
※ 出力層はクラス分類ならクラス数など予め決まった値を設定する．

input_dimは使用するデータのカラムの数を指定する．

※Dense : 通常の全結合ニューラルネットワークレイヤー

```.py
set_seed(42) #出力を固定化するためにランダムシードを設定
model = Sequential()
model.add(Dense(1, input_dim=6)) #入力層と出力層だけの単純パーセプトロン

#多層にする場合は下記のように各
#model = Sequential()
#model.add(Dense(128, input_dim=6))
#model.add(Dense(128))
#model.add(Dense(128))
#model.add(Dense(1))
```


###学習アルゴリズムの設定と損失関数の設定
※ loss  : 損失関数．この値が小さくなるように学習を行う．今回は評価指標と同じRMSE.
※ optimizer : 最適化アルゴリズム．今回は最も使われているAdamを使用．

最適化アルゴリズム，損失関数，評価指標を設定し，学習させる．

```.py
optimizer = Adam()
model.compile(loss=root_mean_squared_error, optimizer=optimizer, metrics=[root_mean_squared_error])
history = model.fit(X, y) #学習モデルの作成
```

<!--
#学習モデルの評価

```.py
score = model.evaluate(X_test, y_test, batch_size=batch_size) #テスト用に分割したデータを学習モデルに適用
score
>
[8.8030, 8.8030] #[損失関数, RMSE]
```

RMSEが小さければ，予測があっている．


実際に中身を見てみる．


```.py
model.predict(X_test) #予測値の表示
>
array([[ 1.1570829 ],
       [-1.1678177 ],
       [ 0.44755507],
       ...,
       [ 1.045417  ],
       [ 0.84293765],
       [ 0.11829198]], dtype=float32)
```

```.py
y_test #正解値の表示
>
9129     1.156143
46791   -1.170171
21163    0.446399
           ...   
10388    1.044189
14773    0.841940
26830    0.117381
Name: SOC, Length: 11447, dtype: float64
```

いくつかの予測した値が正解値となかなか近いことが確認できる．

-->

# テストデータへ適用

今回のデータは，実際には，上記で読み込んだものはすべて訓練用データであり，
テスト用データは別にあるため，テストデータを読み込み，同じ前処理を行って，
学習モデルを適用して，"SOC"の値を出力する．

```.py
test = test_df.drop(["TimeStamp", "Drive Cycle"], axis=1)
test = (test - test.mean()) / test.std() #正規化

SOC_data = model.predict(test) #SOCを予測
SOC_data #中身を見てみる
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189046/a448ba4a-8694-bd9c-04c2-ce43981fa4cb.png)


```.py
#結果をCSVファイルに書き込み
csv_data = pd.DataFrame(data=SOC_data, columns=["SOC"])
csv_data.reset_index(inplace=True)
csv_data = csv_data.rename(columns={'index': 'ID'})
csv_data.to_csv("./data/submission.csv", index=False)
```

./data/submission.csvをダウンロードし，投稿して完了．



#パラメータのチューニングについて

さらにここからパラメーターのチューニングを行う必要があるので，ヒントを示す．

- 機械学習全般
    - 時系列処理
    - 正規化の方法
    - 交差検証
    - 訓練データの損失箇所とテストデータの損失箇所をチェック
    - 使用しなかったデータを使う(DriveCycleなど)
    - 過学習(訓練データに適合しすぎて，テストデータでは予測できない状態)を考慮
    - SOCを調べる(予測する値は何なのか？)

- 深層学習関連
    - レイヤー数とノード数
    - 活性化関数
    - 最適化関数
    - 学習回数(epoch数)
    
- 深層学習以外
    - GridSearchで最適パラメータを見つける



他の学習法を試す場合のサンプル
基本的にはインポートする学習法とその読み込み部分を変更するだけで学習方法を変更することができる．

下記は，最小二乗線形回帰の場合

```.py
import pandas as pd
import glob #ファイル読み込み用
import os
#使用する学習法をインポート．これは，最小二乗線形回帰
from sklearn.linear_model import LinearRegression

df_list = []
#全ファイルをDataFrame形式で読み込む
for filename in glob.glob("./data/train/*.csv"):
    print(filename)
    raw_dataset = pd.read_csv(filename)
    df_list.append(raw_dataset.copy())
df = pd.concat(df_list) #結合

#テストデータの読み込み
filename = "./data/test.csv"
test_df = pd.read_csv(filename)
test_df = test_df.drop(["ID"], axis=1) #IDは不要なので，除去

#ゼロ埋め
df = df.fillna(0)
test_df = test_df.fillna(0)

y = df["SOC"] #正解値のデータのみ抽出
X = df.drop(["SOC", "TimeStamp", "Drive Cycle", "Ah", "Wh"], axis=1) #不要カラムの除去

all_df = pd.concat([df, test_df]) #正規化のため，訓練データとテストデータを結合
all_df = all_df.drop(["SOC", "TimeStamp", "Drive Cycle", "Ah", "Wh"], axis=1) #不要データの除去

X = (X - all_df.mean()) / all_df.std() #訓練データの正規化

lr = LinearRegression() #使用する学習法を選択

lr.fit(X, y) #訓練データ学習

test_df = test_df.drop(["TimeStamp", "Drive Cycle"], axis=1) #不要データの除去
test = test_df
test = (test - all_df.mean()) / all_df.std() #テストデータ正規化

SOC_data = lr.predict(test) #学習モデルにテストデータを適用
SOC_data #結果出力

#CSVファイルに書き込み
csv_data = pd.DataFrame(data=SOC_data, columns=["SOC"])
#0～100の範囲外の値を範囲内に置き換える
csv_data.loc[csv_data['SOC'] < 0, 'SOC'] = 0
csv_data.loc[csv_data['SOC'] > 100, 'SOC'] = 100
csv_data.reset_index(inplace=True)
csv_data = csv_data.rename(columns={'index': 'ID'})
csv_data.to_csv("./data/submission.csv", index=False)

```