ゼロから始める機械学習 上位を狙う編

# 最終目的

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

これまでの勉強会は下記

https://qiita.com/t-baba-11/items/054bda7060952fc6bf74

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

https://qiita.com/t-baba-11/items/e87c078f4e46234df322

https://qiita.com/t-baba-11/items/34f8d2f478504a339481


# 機械学習のチューニングの流れ

コンペで上位を狙うために特徴量を作成する．

※結果は9位だった．

流れ

- ライブラリのインポート
- GPU環境チェック
- 関数設定
- データ読み込み
- 相関があるデータの作成
- 特徴量作成
- 学習モデルの作成
- 結果の出力

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

```.py
import pandas as pd
import numpy as np
import glob #ファイル読み込み用
import os
import random
import matplotlib.pyplot as plt #画像表示のライブラリ
import seaborn as sns
import tensorflow as tf
from tensorflow.keras.models import Sequential #モデル定義用
from tensorflow.keras.optimizers import Adam, Nadam,SGD #最適化アルゴリズム
from tensorflow.keras.layers import Dense, Dropout, BatchNormalization #Kerasのレイヤーのインポート
from tensorflow.keras import backend as K #RMSEを定義するのに使用
from sklearn.model_selection import GroupKFold #交差検証用
```

# GPU環境チェック

GPU環境を設定している場合はdeviceがGPUになっていることを確認．
※ GPU環境は設定が難しいので，うまくいってない場合があるため．
GPUがない場合は，CPUになる．GPUがなくてもプログラムは問題なく動く．

```.py
tf.config.list_physical_devices('GPU')
```


# 関数設定

RMSEとランダムシードの関数を設定

```.py
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)
```

# データ読み込み

```.py
df = pd.DataFrame()
for path in glob.glob(str("./data/train/*.csv")):
    _df = pd.read_csv(path)
    df = pd.concat([df, _df])
df['name'] = df["Chamber_Temp_degC"].astype(str) + '_' + df["Drive Cycle"]
df = df.reset_index(drop=True)

test_df = pd.read_csv("./data/test.csv")
tmp = 0

test_df['name'] = test_df["Chamber_Temp_degC"].astype(str) + '_' + test_df["Drive Cycle"]
print(test_df["name"].unique())

# 欠損値を0埋め
df = df.fillna(0)
test_df = test_df.fillna(0)
df
```


# 相関があるデータの作成

非常に大事な作業．相関係数が1と表示されていても完全な相関ではない．
より，相関のあるデータを作ることが必要になる．

まずは，オリジナル訓練データの相関を確認．

```.py
#各値の相関関係を確認
sns.heatmap(
    df.corr(),
    linewidths=0.1,
    vmax=1.0, 
    square=True,
    cmap=plt.cm.RdBu,
    linecolor='white',
    annot=True
)
```

![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189046/9e759a6a-01ed-e57a-d3f1-2138714d7697.png)

AhとWhはテストデータにないので，使えない．
相関があるカラムを作る必要がある．

※ 特徴量の移動平均をとるべきか？

```.py
columns = ['Battery_Temp_degC', 'Chamber_Temp_degC', 'Current', 'Power', 'Time', 'Voltage', 'pred_SOC', 'pred_Wh'] #累積和を求めるカラムを決める

#CurrentとPowerはAhとWhに近くなるように計測時間の間隔毎に求める．
df['pred_SOC'] = df['Current']*df.groupby('name')['Time'].diff()/3600
df['pred_Wh'] = df['Power']*df.groupby('name')['Time'].diff()/3600

cumsum_df = pd.DataFrame({
    'name': df['name'].values,
    'SOC': df['SOC'].values
})


#累積和を求める
cumsum_df = pd.concat([
    cumsum_df,
    df.groupby('name')[columns].cumsum().add_prefix('cumsum_') #わかりやすいようにprefixを付ける
], axis=1)

#Currentからで，SOCの予測を求める．
cumsum_df['est_SOC'] = (2.9+cumsum_df['cumsum_pred_SOC'])/2.9*100
cumsum_df['est_SOC'] = cumsum_df['est_SOC'].fillna(100)


#累積和の相関関係を確認
sns.heatmap(
    cumsum_df.corr(),
    linewidths=0.1,
    vmax=1.0, 
    square=True,
    cmap=plt.cm.RdBu,
    linecolor='white',
    annot=True
)
```

![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189046/7c004abb-6002-97a8-4732-7b24ae565928.png)

単純な累積和である，cumsum_Current, cumsum_Power．
時間を計算に含めた，cumsum_pred_SOC,cumsum_pred_Wh．
SOC計算式で，計算したest_SOC．
これらは，SOCと非常に相関高い．

一応オリジナルデータも加えて相関を確認してみる．

```.py
#オリジナルデータも結合
a_df = pd.concat([cumsum_df.drop('SOC', axis=1), df], axis=1)

#全体の相関を確認
sns.heatmap(
    a_df.corr(),
    linewidths=0.1,
    vmax=1.0, 
    square=True,
    cmap=plt.cm.RdBu,
    linecolor='white',
    annot=True
)
```

![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189046/fc7321c8-b1be-a812-b263-3e168062bd0a.png)

いくつか，ペアプロットして確認してみる．
特に，est_SOCはSOCとの相関が高いことが分かる．

```.py
temp_df = a_df[['SOC', 'Ah', 'cumsum_Current', 'est_SOC', 'cumsum_pred_Wh', 'cumsum_Voltage']]
sns.pairplot(temp_df)
```

![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189046/42ed17ca-b62c-270a-de9c-11a994da887f.png)

ドライブサイクルとthermal chamberの温度毎に計測時間でのSOCを確認する．

```.py
#ドライブサイクルとthermal chamberの温度毎に時間計測でのSOCの減退率が違う
fig = plt.figure(figsize=(15,40))

length = len(df["name"].unique())

for i, name in enumerate(df["name"].unique()):
    data = df[df['name'] == name]
    plt.subplot(length, 1, i+1)
    graph_title = data["Drive Cycle"].iloc[0] +" : "+str(data["Chamber_Temp_degC"].iloc[0])
    g = sns.lineplot(data=data[:],x="Time",y="SOC")
    g.set_title(f'{graph_title}', fontsize=10)
    plt.ylim(0,100)

    
plt.tight_layout()
plt.show()
```

![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189046/b60855b4-4af8-1974-0463-025112ce8960.png)

ドライブサイクルとthermal chamberの温度の情報は，特徴量として，使用するのがよさそう．
しかしドライブサイクルは，訓練データとテストデータで違うため，使用できない．
thermal chamberの温度だけでもカテゴリデータとして使用すべきか．

# 特徴量作成

作成したデータのうち実際に使用する特徴量を作成する．

```.py

# 特徴量作成
#use_colum = ['Current', 'Power', 'Time', 'Voltage']
#use_colum = ['Current', 'Power']
#use_colum = ['Current']
cumsum_use_colum = ['pred_Wh', 'pred_SOC', 'Voltage'] #累積和を求めるカラムを設定
#use_colum = ['cumsum_Power', 'est_SOC']
use_colum = ['est_SOC', 'cumsum_pred_Wh', 'cumsum_Voltage'] #使用するカラムを設定
#use_colum = ['est_SOC', 'cumsum_pred_Wh'] #使用するカラムを設定


#訓練データの成型
X = df.groupby('name')[cumsum_use_colum].cumsum().add_prefix('cumsum_')
X['est_SOC'] = (2.9+X['cumsum_pred_SOC'])/2.9*100 #予測SOCを計算
X.loc[X['est_SOC'] < 0, 'est_SOC'] = 0
X.loc[X['est_SOC'] > 100, 'est_SOC'] = 100

X = X[use_colum]
X = pd.concat([X[use_colum],df[['Chamber_Temp_degC']]], axis=1) #Chamber_Temp_degCのカラムを追加

#テストデータの成型
test_df['pred_SOC'] = test_df['Current']*test_df.groupby('name')['Time'].diff()/3600 #予測Ahを計算
test_df['pred_Wh'] = test_df['Power']*test_df.groupby('name')['Time'].diff()/3600 #予測Whを計算
test = test_df.groupby('name')[cumsum_use_colum].cumsum().add_prefix('cumsum_')
test['est_SOC'] = (2.9+test['cumsum_pred_SOC'])/2.9*100 #予測SOCを計算
test.loc[test['est_SOC'] < 0, 'est_SOC'] = 0
test.loc[test['est_SOC'] > 100, 'est_SOC'] = 100
test = test[use_colum]
test = pd.concat([test[use_colum],test_df[['Chamber_Temp_degC']]], axis=1)

X['est_SOC'] = X['est_SOC'].fillna(100)
test['est_SOC'] = test['est_SOC'].fillna(100)
X = X.fillna(0)
test = test.fillna(0)

print(X)

#正規化
y = df['SOC']
all_df = pd.concat([X, test])
X = (X - all_df.mean()) / all_df.std()
test = (test - all_df.mean()) / all_df.std()
```


# 学習モデルの作成

パラメーターチューニングの箇所が多いので，注意しながら，学習モデルを作る．
学習方法は他にも試す．

実行したら非常に時間がかかるので，待つ．1日ぐらいかかる場合もある．


```.py
models = []
train_check = np.zeros(len(X), dtype=np.float64)

set_seed(42) #出力を固定化するためにランダムシードを設定

es_cb = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=20, verbose=1, mode='auto')

valid_scores = []

#set_seed(42) #出力を固定化するためにランダムシードを設定
print(tf.random.uniform([1]))
model = Sequential()
model.add(Dense(128, input_dim=X.shape[1]))
model.add(Dense(4096)) #ノード数を多くすることで，1階層で学習できるようにしてみる
model.add(Dense(1)) #出力層

optimizer = Adam(decay=0.001)
model.compile(loss=root_mean_squared_error, optimizer=optimizer, metrics=[root_mean_squared_error])

m = model

#実際の交差検証とは違うが，分割したデータごとに学習回数を変えて学習させる    
kf = GroupKFold(n_splits=5) # cross validationはGroupKFoldで行う
for fold, (tr_idx, va_idx) in enumerate(kf.split(X, y, df['name'])):
    x_train, y_train = X.iloc[tr_idx], y[tr_idx]
    x_valid, y_valid = X.iloc[va_idx], y[va_idx]
    m.fit(x_train, y_train, batch_size=512,  epochs=300, validation_data=(x_valid, y_valid), callbacks=[es_cb])
    
train_check += m.predict(X)[:, 0]    
train_check = np.where(train_check > 100, 100, train_check)
train_check = np.where(train_check < 0, 0, train_check)


score = root_mean_squared_error(y, train_check)
print(score)
```

# 結果出力

結果を計算しファイルに出力して，そのファイルを提出．

```.py
SOC_data = np.zeros(len(test), dtype=np.float64)
#predictの結果が先ほどと同様に違うので，下記を変更．
SOC_data += np.array([model.predict(test)[:, 0] for model in models]).mean(axis=0) #深層学習の場合．
#SOC_data += np.array([model.predict(test) for model in models]).mean(axis=0) #深層学習以外の場合．
#CSVファイルに書き込み
csv_data = pd.DataFrame(data=SOC_data, columns=["SOC"])
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)
```