Kaggleのタイタニック問題で、スコア80.143までの記録(その1)
Contents
Kaggleのタイタニック問題
Kaggle の入門コンペティションでタイタニック号の乗客リストから生存者を予測する問題でスコアが目標の 80.0% を超えたので記録として記事にしておく。
尚、Kaggle をやった人なら分かるとは思うが、タイタニック問題で 80% という数字はそれ程凄い数字では無い。
また提出回数も 27回なので特段少ない回数では無いが、自分が試行錯誤したプロセスが誰かの役に立つのでは無いかと思い、記事にすることにした。
尚、順位は 473位だった。
ちなみに1位から 160位ぐらいまでは正解率 100%(そして提出回数は1回など)という通常の数理統計や機械学習などで予測していたらあり得ない数字なので、どこかで正解データを入手してアップしただけだと思われる。
そんな事をしても意味が無いと思うのだけれでも、まぁ、本人にとっては何か意味があるのだろう。。。
最終的な手法
最初に最終的な手法を記しておく。
表形式の構造化データを扱う問題の場合、DNN(Deep Neural Network)よりもランダムフォレストやXGBoost 等の機械学習の方が精度がでるらしく、タイタニック問題をネットで調べると、ランダムフォレストが主流だった。
自分は、Deep Learning でやりたかったので、DNN(Deep Neural Network)で 0.80 超えを目指したのだが思ったより手強かった。
仕方が無いのでタイタニック問題では実績のあるランダムフォレストも試してみて複数の手法での平均値から予測を行って 0.80 超えを目指した。
実は DNN とランダムフォレストとの予測確率(predict_proba)の平均から 0.80 は超えていたのだが、ついでに XGBoost も試してみている。
しかし XGBoost は思ったよりスコアが上がらなかったので最終的には予測結果は使用しなかったのだが、一応記事としては書いておく。
データ分析
まずは、train.csv をベースにざっくりとしたデータ分析を行って全体の傾向と、どの項目が生存率に影響するのかを探る。
グラフを表示するプログラムは後述する。
全体生存率
乗客全体の生存率は 38.4%とかなり低い。
改めて数字で見ると生存確率の低い大事故であった事が分かる。
男女別生存率
女性の生存確率、74.2% と比べて男性の生存率が 18.89% とかなり低かった事が分かる。
恐らく女性が優先に救命ボートに乗せられたのだと思われる。
性別(Sex)は生存予測の重要な項目だ。
チケットクラス別生存率
Pclass(チケットクラス)は1、2、3 の3種類。
1 > 2 > 3 の順に生存率が下がっている事が分かる。
上位のチケットクラスの方が生存率が高い。
乗船場別生存率
embarked(乗船場)別の生存率。
- C = Cherbourg(シェルブール)
- Q = Queenstown(クイーンズタウン)
- S = Southampton(サウサンプトン)
タイタニック号はイギリスのサウサンプトン港からフランスのシェブール、アイルランドのクイーンズタウン(現・コーヴ)を経由してニューヨークに向かった。
出発港のサウサンプトン港から乗船した乗客が一番生存率が低くてフランスのシェルブールから乗船した乗客は比較的生存率が高い。
年齢別生存率
年齢(Age)を 10 で割って年代に変換して生存率を表示した。
0代(0~9歳)は生存率が高く、概ね年齢が上がるにつれて生存率が下がっている。
やはり女性や子供は優先的に助けられたのだと思う。
尚、Age(年齢)データは欠損値が多いので何らかの方法で埋める必要がある。
当初は、性別、チケットクラス別に平均年齢を算出して埋めたが、後に Deep Neural Network で予測した値を埋めるようにしたら多少だが予測スコアが上がった。
詳細は後述する。
料金別生存率
料金(fare)別の生存率(年齢と同様に 10で割っている)
やはり料金が上がっていくにつれて生存率が上がっている。
10 ポンド以下の生存率はかなり低い。
titanic_data_analyze.py
上記のグラフ化プログラムは以下の通り。
# -*- coding: utf-8 -*-
"""
Created on Wed Sep 29 11:50:59 2021
@author: Souichirou Kikuchi
titanic_data_analyze.py
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# GDRIVE = '/content/drive/MyDrive/M2B/Program/Kaggle/Titanic' # Google colab
GDRIVE = '.' # Local Python
# データ集計
def data_aggregation(col, data):
data = data.dropna(subset=[col]) # 対象列にnanがあれば削除
uq_list = data[col].unique() # 対象項目の一覧(ユニーク)
uq_list = np.sort(uq_list) # 昇順にソート
prob_suv = np.zeros((len(uq_list), len(sv_list)))
for ix, i_sex in enumerate(uq_list):
prob_suv[ix][0] = ((data[col] == i_sex) & (data['Survived'] == 1)).sum() / (data[col] == i_sex).sum() # 生存率
prob_suv[ix][1] = 1 - prob_suv[ix][0] # 死亡率
return uq_list, prob_suv
# グラフ描画
def graph_drawing(x_label, y_label, x_ax_list, y_ax_list, graph_data, text_visible):
fig, ax = plt.subplots(figsize=(10, 8))
ax.set(xlabel = x_label, ylabel = y_label) # ラベル表示
for x in range(len(x_ax_list)): # X軸
bottoms = 0
for y in range(len(y_ax_list)): # Y軸
# バーを描画
ax.bar(x_ax_list[x], # X軸位置
[graph_data[x][y]], # 値
color = 'C' + str(y), # 色
bottom = bottoms) # 開始底辺位置
if text_visible == True:
# バー内にテキスト
ax.text(x = x_ax_list[x], # X軸位置
y = (graph_data[x][y] / 2) + bottoms, # Y軸位置
s = y_ax_list[y] + '\n({:.2f} %)'.format(graph_data[x][y] * 100), # Text
ha = 'center', # 水平位置
va = 'center') # 垂直位置
bottoms = graph_data[x][y] # 底辺位置の変更
plt.show()
plt.clf()
# main
train_data = pd.read_csv(GDRIVE + '/csv/train.csv', index_col=False) # CSVの読み込み
# 全体の生存率を表示
sv_count = (train_data['Survived'] == 1).sum() # 生存者数
di_count = (train_data['Survived'] == 0).sum() # 死亡者数
sv_list = ['survived', 'died'] # 生存・死亡リスト
x = np.array([sv_count, di_count])
fig, ax = plt.subplots(figsize=(10, 8))
ax.pie(x,
labels = sv_list, # ラベル
startangle = 90, # 表示開始角度
autopct = '%1.1f%%', # 構成割合を%表示
counterclock = False, # 反時計回りで表示
wedgeprops = {'linewidth': 3, 'edgecolor':'white'}) # 枠線(幅と境界線の色)
plt.show()
# 男女別の生存率を表示
uq_list, prob_suv = data_aggregation(col = 'Sex', data = train_data) # 男女別の生存率を集計
# 描画
graph_drawing(x_label = 'Sex', # X軸のラベル
y_label = 'probability of survival', # Y軸のラベル
x_ax_list = uq_list, # X軸に表示する項目のリスト
y_ax_list = sv_list, # Y軸に表示する項目のリスト
graph_data = prob_suv, # 集計データ(X:集計したい項目、Y:生存・死亡)
text_visible = True) # 数値を表示する
# Pclass別の生存率を表示
uq_list, prob_suv = data_aggregation(col = 'Pclass', data = train_data) # Pclass別の生存率を集計
# 描画
graph_drawing(x_label = 'Pclass', # X軸のラベル
y_label = 'probability of survival', # Y軸のラベル
x_ax_list = uq_list, # X軸に表示する項目のリスト
y_ax_list = sv_list, # Y軸に表示する項目のリスト
graph_data = prob_suv, # 集計データ(X:集計したい項目、Y:生存・死亡)
text_visible = True) # 数値を表示する
# Embarked別の生存率を表示
uq_list, prob_suv = data_aggregation(col = 'Embarked', data = train_data) # Embarked別の生存率を集計
# 描画
graph_drawing(x_label = 'Embarked', # X軸のラベル
y_label = 'probability of survival', # Y軸のラベル
x_ax_list = uq_list, # X軸に表示する項目のリスト
y_ax_list = sv_list, # Y軸に表示する項目のリスト
graph_data = prob_suv, # 集計データ(X:集計したい項目、Y:生存・死亡)
text_visible = True) # 数値を表示する
# 年齢別の生存率を表示
train_data['Age_s'] = train_data['Age'] // 10 # 10で割って年代を計算
uq_list, prob_suv = data_aggregation(col = 'Age_s', data = train_data) # 年代別の生存率を集計
# 描画
graph_drawing(x_label = 'Age', # X軸のラベル
y_label = 'probability of survival', # Y軸のラベル
x_ax_list = uq_list, # X軸に表示する項目のリスト
y_ax_list = sv_list, # Y軸に表示する項目のリスト
graph_data = prob_suv, # 集計データ(X:集計したい項目、Y:生存・死亡)
text_visible = True) # 数値を表示する
# 料金別の生存率を表示
train_data['Fare_s'] = train_data['Fare'] // 10 # 10で割る
uq_list, prob_suv = data_aggregation(col = 'Fare_s', data = train_data) # 料金別の生存率を集計
# 描画
graph_drawing(x_label = 'Fare', # X軸のラベル
y_label = 'probability of survival', # Y軸のラベル
x_ax_list = uq_list, # X軸に表示する項目のリスト
y_ax_list = sv_list, # Y軸に表示する項目のリスト
graph_data = prob_suv, # 集計データ(X:集計したい項目、Y:生存・死亡)
text_visible = False) # 数値を表示しない
補足説明
詳細はプログラム中にコメントを入れているが、簡単に補足説明を書いておく。
GDRIVE
定数 GDRIVE は同じプログラムで動作環境をローカルと Google Colab とで切り替える為に使用している。
出先で高速なパソコンを使えない時は Google Colab で予測を行っていたので簡単に環境を切り替えるために定数を使った。
Google Colab で Google Drive に接続すると、”/content/drive/MyDrive/~” 配下にマウントされる。
読み込む CSV ファイルを Google Drive 上に保存してどちらの環境からでも上記の定数を切り替えて読み込めるようにしている。
data_aggregation
データを集計して生存率を計算する部分を共通化した関数。
パラメーター data から col 列の値ごとの生存率を計算して、以下の値を返す。
uq_list | col 列に入っている値をユニークにした一覧。 例) 性別(Sex)なら [“female”, “male”] Pclassなら[1, 2, 3] 尚、対象の項目が nan のデータは事前に削除している | ||||
prob_suv | 生存率・死亡率 × uq_list の配列 列:生存率、死亡率の2列 行:上記の uq_list 分の行が作成される
|
graph_drawing
100% の積み上げ棒グラフを表示する関数。
X軸に項目の値、Y軸に生存率と死亡率(合計で1.0になる)を表示する。
1の棒グラフを描いたら1の頂点を2のボトムに設定して2を描き、右に移動して3を描いて、3の頂点をボトムにして4の順番で描いてる。
また同時にテキストで値をパーセント表示している。
# main
全体の生存率・死亡率の円グラフは関数を使わずに描いている。
その後の、男女別、チケットクラス別、乗船場別、年齢別、料金別の100%積み上げ棒グラフはデータ集計とグラフ描画の関数を使っている。
スプレッドシートへの読み込み
尚、データ量が多い時は難しいが今回程度のデータ量であれば Google Spread Sheet に読み込んで全体を眺めたり、手動でフィルターを設定してみるとデータの傾向や分布が把握しやすかった。
注意点としてはデータ中の ,(カンマ)がスプレッドシート上では見えなくなってしまっている。
具体的にはCSVデータ上では、Name欄の姓の後に ,(カンマ)が入っているのだが取り込んだ時に消えてしまっているので注意が必要だ。
Deep Neural Networkでの予測
続いて Deep Neural Network で乗客の生存率を予測する。
尚、Deep Neural Network の構築ではイモリの表紙の書籍「scikit-learn、Keras、TensorFlowによる実践機械学習 第2版」を参考にさせて貰った。
特に、この後に出てくる最適なハイパーパラメータの検索やDeep Neural Network の訓練など、10章、11章の内容は大変参考になった。
また SVM、決定木、ランダムフォレストについても具体的なソースコードが載っていて参考になったのと、まだあまり読んでいないが、GAN、強化学習についても2版から追加掲載されているのでオススメの書籍だ。
年齢を予測する
データの事前チェックを行った所、Age(年齢)には、891件中 177件の欠損データがあった。
当初は性別とPclass(チケットクラス)の平均をセットして欠損値を埋めていたのだが、Age を Deep Neural Network で予測してセットした所、最終的な予測精度が向上した。
よって、ちょっと面倒だったが最初に欠損値のAge(年齢)を予測するDeep Neural Network を構築して欠損値を埋めた後で生存率を予測するモデルを作成することにした。
生存率を予測するまでの全体の流れを図にすると以下になる。
- Age(年齢)の最適なハイパーパラメータを検索する
- 学習してモデルを作成する
- 欠損値を埋める
- 生存率の学習の為の最適なハイパーパラメータを検索する
- 生存率モデルを学習する
- 生存率を予測する
最適なハイパーパラメータの検索
ニューラルネットワークを構築するに辺り、大切なのはハイパーパラメータの設定だ。
今までは人のモデルを参考にして「何となくこれぐらいの値」という感じで経験と勘で設定してきた。
今回は Age 予測用のコードの前に最適なハイパーパラメータ(層の数、ニューロン数、学習率、L2正則化率)を RandomizedSearchCV と Hyperas で検索した。
尚、 RandomizedSearchCV の使い方については先程の書籍「scikit-learn、Keras、TensorFlowによる実践機械学習 第2版」を参考にした。
Hyperas も先程の書籍に載っていて、「ハイパーパラメータを最適化するライブラリーで Keras モデルを最適化するのに適している」との事だったが、結論を先に言うと RandomizedSearchCV の方が時間は掛かるがより良いハイパーパラメータを探して来てくれたのでこちらを使用している。
RandomizedSearchCVでの検索
RandomizedSearchCV を使った最適なハイパーパラメータを検索するプログラムソースコードは以下の通り。
age_data.py
Age データの前処理を行うモジュールと関数。
# -*- coding: utf-8 -*-
"""
Created on Mon Nov 1 07:52:03 2021
@author: Souichirou Kikuchi
"""
def a_data(drive, r_state):
from sklearn.preprocessing import LabelEncoder # 文字列を数値に置き換える
from sklearn.preprocessing import StandardScaler # 標準化
from sklearn.preprocessing import MinMaxScaler # 正規化
from sklearn.model_selection import train_test_split # データを分割
import pandas as pd
train_csv = pd.read_csv(drive + '/csv/train.csv', index_col=False) # CSVの読み込み
test_csv = pd.read_csv(drive + '/csv/test.csv', index_col=False) # CSVの読み込み
tr_ts = pd.concat([train_csv, test_csv], ignore_index=True)
# Ageを推定可能な項目を取り出す
age_df = tr_ts[['Age', 'Name', 'Pclass', 'Sex', 'Parch', 'SibSp']]
# Nameから敬称(Mr.等を取り出す)
age_df['Name_ttl'] = age_df['Name'].str.extract(r'(\S+)\.', expand=False) # ピリオドで終わる文字列を取り出す
# 性別、敬称の数値化
for col in ['Sex', 'Name_ttl']:
le = LabelEncoder()
le.fit(age_df[col])
age_df[col] = le.transform(age_df[col])
# 正規化する項目のリスト
ms_ss_list = ['Pclass','Sex','Parch','SibSp', 'Name_ttl']
ms = MinMaxScaler(feature_range=(0, 1)) # データの正規化(0-1の範囲)
age_df.loc[:,ms_ss_list] = ms.fit_transform(age_df[ms_ss_list])
ss = StandardScaler() # データの標準化(正規分布に変換)
age_df.loc[:,ms_ss_list] = ss.fit_transform(age_df[ms_ss_list])
drop_list = ['Name'] # 不要項目の削除
for dl in drop_list:
age_df.drop(columns=[dl], inplace=True)
# Ageがnot nullとnullのデータセットに分割
age_nn = age_df[age_df.Age.notnull()].values
age_nl = age_df[age_df.Age.isnull()].values
# Not nullのデータを学習データにする
train = age_nn[:, 1:] # 学習データ
age = age_nn[:, 0] # 正解ラベル
age_pr = age_nl[:, 1:] # nullの予測したいデータセット
if r_state == True:
X_train, X_test, y_train, y_test = train_test_split(train, # 学習データ
age, # 正解ラベル
train_size = 0.8, # trainibg
test_size = 0.2, # Validation
random_state = 42, # 毎回固定(モデルの比較時など)
shuffle = True) # 取り出す前にシャッフルする
else: # random_state未指定
X_train, X_test, y_train, y_test = train_test_split(train, # 学習データ
age, # 正解ラベル
train_size = 0.8, # trainibg
test_size = 0.2, # Validation
shuffle = True) # 取り出す前にシャッフルする
return X_train, X_test, y_train, y_test, age_pr
a_data 関数
敢えて関数にしているのは、後ほど Hyperas でも最適なハイパーパラメータの検索を行うのだが、Hyperas では optim.minimize の パラメータに学習データ(X_train, X_test, y_train, y_test)を返す関数が必要だったので関数化した。
なるべく同じ様なソースコードにしたかったのでこちらも学習データを作成する部分を関数化している。
また、Age(年齢)を予測する特徴量として以下の項目を選択している。
Name | 名前の敬称(Mr. とか Mis. Miss.)の部分を取り出して使用した。 例えば Miss. やMaster.(少年)であれば年齢が低いことが予想されるので特徴量に加えた。 |
Pclass | チケットクラス毎の平均年齢、
チケットクラスが低くなるにつれて平均年齢が低くなっている。 恐らく高齢の富裕層が1等、若者達が3等に多く乗ったのだと想像される。 これも年齢を予測する特徴量になると思い、加えた。 |
Sex | 性別 女性の平均が 27.91歳、男性の平均が 30.72歳だったので女性の方が若干年齢が低い。 ある程度、年齢を予測する際の特徴量になると思い項目に加えた。 |
Parch | 親、子供の数による平均年齢、
親、子供が同乗していない人の方が平均年齢が高い。 子供を連れて乗っているので平均年齢が下がっているのだと予想される。 |
SibSp | 兄弟、配偶者の数による平均年齢、
こちらも Parch と同様の傾向が見られたので特徴量に加えた。 |
また train_csv と test_csv を合わせて一つの CSV として処理しているのは、名前から敬称(Mr. とかMiss.など)を取り出して数値化する際に片側にしか存在しない敬称があると、正しく学習できないと思い、一つにしている。
正規化と標準化
また全ての項目は 0~1 の範囲に正規化したうえで標準化(正規分布化)している。
# 正規化する項目のリスト
ms_ss_list = ['Pclass','Sex','Parch','SibSp', 'Name_ttl']
ms = MinMaxScaler(feature_range=(0, 1)) # データの正規化(0-1の範囲)
age_df.loc[:,ms_ss_list] = ms.fit_transform(age_df[ms_ss_list])
ss = StandardScaler() # データの標準化(正規分布に変換)
age_df.loc[:,ms_ss_list] = ss.fit_transform(age_df[ms_ss_list])
StandardScaler() で標準化すると値から平均値を引いて、その値を分散で割るので、マイナスの値が発生する。
ニューラルネットワークでは 0~1 の値に収まっている方が学習しやすいとの記述も見かけたが、結果として標準化した方がスコアが良かったので標準化している。
尚、学習用データはトレーニング(学習):バリデーション(検証)を8:2で分割した。
random_state = 42
パラメータ(r_state)で学習と検証データに分割する際に固定の random_state を指定するかどうかを分けている。
random_state に固定の値を設定すると毎回同じ様にシャッフルして学習と検証データを取り出すのでモデル間の比較ができる。
RandomizedSearchCV で最適なハイパーパラメータの検索を複数回実行する際は毎回異なるシャッフルをすると正しく比較ができなくなると思い固定値を設定した。
一方、ハイパーパラメータが決まった後で学習モデルを作成する際はランダムに学習データと検証データを分離したいので r_state に False を設定する。
ちなみに固定値の時に 0 や 42 を設定している事が多い。
42 は映画「銀河ヒッチハイク・ガイド」でスーパーコンピューターが 750万年かけて導き出した “万物についての究極の疑問の答え” らしい。
ギークな人たちが好きな数字なのかも知れない。
age_model.py
モデルを作成する関数。
# -*- coding: utf-8 -*-
"""
Created on Mon Nov 1 08:02:19 2021
@author: Souichirou Kikuchi
"""
# モデルの作成
def a_build_model(n_hidden, n_neurons, learning_rate, l2_rate, input_shape=[5]):
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, InputLayer, Dropout
from tensorflow.keras import regularizers # 正則化
from tensorflow.keras import optimizers
REGULARIZATION = regularizers.l2(l2_rate) # 正則化:L2、正則化率
LOSS = 'mse'
METRICS = 'mse'
DROP_RATE = 0.2
model = Sequential()
model.add(InputLayer(input_shape=input_shape))
for layer in range(n_hidden):
model.add(Dense(n_neurons,
activation='sigmoid',
kernel_regularizer= REGULARIZATION))
model.add(Dropout(DROP_RATE))
model.add(Dense(1, activation='linear'))
optimizer = optimizers.Adam(lr=learning_rate)
model.compile(loss=LOSS, optimizer=optimizer,metrics=[METRICS]) # 精度
return model
a_build_model 関数
- n_hidden:隠れ層の数
- n_neurons:ニューラル数
- learning_rate:学習率
- l2_rate:L2正則化の際の正則化率
上記のハイパーパラメータの最適値を RandomizedSearchCV で検索する。
損失関数
損失関数、評価関数には “mse”(平均二乗誤差)を使用した。
mse(mean squared error)は回帰問題で良く使う損失関数で、外れ値に過剰適応(過学習)してしまうという注意点があるが、今回の様に年齢を予測するモデルには最適と判断した。
ドロップアウト率
ドロップアウト率は固定で 0.2 としている。
ドロップアウト率も当初は RandomizedSearchCV で最適値を検索していたのだが、0.001 等、かなり低い数値が Best Parameter として表示されてしまった。
先程の書籍「scikit-learn、Keras、TensorFlowによる実践機械学習 第2版」によると、
- Drop率は一般的には 10% ~ 50%
- 再帰型ネットワークでは 20% ~ 30%
- モデルが過学習しているようであればドロップアウト率を上げて、逆なら下げる
- 大規模な層ではドロップアウト率を上げて小規模では下げる
とあったので、小規模なネットワークと判断して 0.2 の固定とした。
また、よく見かける Dropアウト層、隠れ層、 Dropアウト層、隠れ層、・・・ の様に複数のDropアウト層のモデルにせずに、最後の出力層の前だけに Dropアウト層を入れている。
これも先程の書籍に「最新のアーキテクチャの多くは、ドロップアウトを最後の隠れ層のあとだけで使っている・・・」との記述があったので真似をした所、少しだけスコアが上昇した。
隠れ層
隠れ層は、下記の様に for 文で作成している。
n_hidden の数が隠れ層の数、n_neurons がニューロン数になる。
for layer in range(n_hidden):
model.add(Dense(n_neurons,
activation='sigmoid',
kernel_regularizer= REGULARIZATION))
この作り方だと全ての隠れ層のニューロン数は同じ値になる。
よく見かけるモデルは 64 → 32 → 8 の様に出力層に近づくに従ってニューロン数を減らしているモデルだ。
これも先程の書籍によると「従来の方法(出力層に近づくにつれてニューロン数を減らす)は使われなくなってきている。すべての隠れ層で同じ数のニューロンを使っても、多くの場合はうまく機能し、かえってその方が良い場合さえある」とあったので、その様にしている。
ただ「最初の隠れ層をほかの隠れ層よりも大きくすると効果的な場合がある」ともあったので、最初の隠れ層だけ大きな値を設定するのは試してみる価値があると思う。
活性化関数
活性化関数は sigmoid 関数を使用した。
activation='sigmoid',
relu 関数でも良かったのだが、色々な活性化関数を試した所 sigmoid 関数の結果が良かったので使っている。
ただ、先程の書籍では、「ケース・バイ・ケースではあるが、SELU > ELU > leaky ReLU(およびその亜種) > ReLU > tanh(双曲線正接) > ロジスティク」の順番で推奨となっていた。
正則化
過学習の防止の為に正則化を使用している。
正則化は L2正則化を使用していて、最適な正則化率を l2_rate で検索している。
出力層
出力層は数値(年齢)を予測したいので出力数は 1 でlinear(線形)を指定している。
model.add(Dense(1, activation='linear'))
titanic_dnn_age_optimize.py
main ルーチンの説明。
# -*- coding: utf-8 -*-
"""
Created on Sat Oct 9 08:18:51 2021
RandomizedSearchCVで最適なハイパーパラメーターを探す
Ageを'Pclass','Sex','Parch','SibSp'から推定するモデル
@author: Souichirou Kikuchi
"""
# GDRIVE = '/content/drive/MyDrive/M2B/Program/Kaggle/Titanic' # Google colab
GDRIVE = '.' # Local Python
import sys # 自作モジュールへのPathを追加する(colabで必要)
ROOT_PATH = GDRIVE
sys.path.append(ROOT_PATH)
from age_data import a_data # データ前処理
from age_model import a_build_model # モデル作成
import matplotlib.pyplot as plt
#main
X_train, X_test, y_train, y_test, age_pr = a_data(drive = GDRIVE, r_state = True) # データ前処理
BATCH_SIZE = 10 # バッチサイズ
EPOCHS = 100 # エポック数
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.wrappers.scikit_learn import KerasRegressor
# チェックポイント
# cp = ModelCheckpoint(GDRIVE + '/models/titanic_age_model_' + VERSION + '/age_predict_model.h5',
# save_best_only=True) # 最良モデルの保存
#過学習を防ぐための早期終了
es = EarlyStopping(monitor='val_loss',
patience=5, # このepoch数、性能が向上しなければストップ
restore_best_weights=True) # 最適な重みを保存する
keras_reg = KerasRegressor(a_build_model)
from scipy.stats import reciprocal
from sklearn.model_selection import RandomizedSearchCV
import numpy as np
param_distribs = {
'n_hidden': [1, 2, 3, 4, 5],
'n_neurons': np.arange(8, 64),
'learning_rate': reciprocal(3e-4, 3e-1),
'l2_rate': reciprocal(3e-4, 3e-1)
}
rnd_search_cv = RandomizedSearchCV(estimator = keras_reg, # 推定するオブジェクト
param_distributions = param_distribs, # パラメータ
n_iter = 20, # サンプリングするパラメータの数。大きくすると解の質は良くなるが時間が掛かる
cv=5) # 交差検証の数
rnd_search_cv.fit(X_train, # 訓練用データ
y_train, # 訓練用ラベル
epochs=EPOCHS, # エポック数
validation_data=(X_test, y_test), # Validationデータ
batch_size=BATCH_SIZE, # バッチサイズ
verbose=1, # 実行状況表示
callbacks=[
# cp, # チェックポイント
es] # 早期終了
)
print(rnd_search_cv.best_params_)
print(rnd_search_cv.best_score_)
model = rnd_search_cv.best_estimator_.model # ベストなモデルを選択
# 未知のValidationデータで学習済みモデルの汎化性能を評価
score = model.evaluate(X_test, y_test, batch_size=BATCH_SIZE)
print('vali loss =', score[0])
print('vali accuracy =', score[1])
pre = model.predict(age_pr)
print(pre)
plt.figure(figsize=(18, 9)) # 予測をグラフ化
plt.plot(pre, color='r', label='Predict')
plt.legend()
plt.show()
colab用
Google colab で動作させる時の自作モジュール(age_data.py、age_model.py)をノートブックから認識させるために Google Drive 上のモジュールの格納場所の Path を追加している。
import sys # 自作モジュールへのPathを追加する(colabで必要)
ROOT_PATH = GDRIVE
sys.path.append(ROOT_PATH)
バッチサイズなど
データの前処理を行う関数を呼び出した後、バッチサイズとエポック数を指定している。
BATCH_SIZE = 10 # バッチサイズ
EPOCHS = 100 # エポック数
エポック数を 100 と大きくしているのは早期終了(EarlyStopping)をしているからである(後述)
エポック数を大きな数字にしておいて学習が進まなくなった所で早期に終了させている。
チェックポイント
チェックポイントは学習中のモデルを保存しておく機能で、最良モデルを保存する事ができる。
最適なハイパーパラメータを検索する際は使用しないのでコメントアウトしている。
早期終了
性能が向上しなくなったらエポック途中でも学習を中止する為の指定。
es = EarlyStopping(monitor='val_loss',
patience=5, # このepoch数、性能が向上しなければストップ
restore_best_weights=True) # 最適な重みを保存する
検証データの損失(val_loass)が5回連続して向上しなければ停止して、それまでの最適な重みを保存しておく。
パラメータ
最適値を検索したいパラメータを指定する。
param_distribs = {
'n_hidden': [1, 2, 3, 4, 5],
'n_neurons': np.arange(8, 64),
'learning_rate': reciprocal(3e-4, 3e-1),
'l2_rate': reciprocal(3e-4, 3e-1)
}
ここでは、
- n_hidden:隠れ層の数の候補
- n_neurons:ニューロン数
- learning_rate: 学習率
- l2_rate:L2正則化の正則化率
の組み合わせで最適なパラメータを検索しているが、数値の他に活性化関数なども検索できる。
RandomizedSearchCV
RandomizedSearchCV で最適なパラメータを検索する。
rnd_search_cv = RandomizedSearchCV(estimator = keras_reg, # 推定するオブジェクト
param_distributions = param_distribs, # パラメータ
n_iter = 20, # サンプリングするパラメータの数。大きくすると解の質は良くなるが時間が掛かる
cv=5) # 交差検証の数
n_iter のデフォルトは 10 だが、20 に設定している。
数を大きくすると時間は掛かるが、より多くのサンプリングをするので最適なパラメータを見つけてくる可能性が高まるので 20 を設定した。
fit
訓練用データと正解ラベル、検証用データを与えて学習する。
チェックポイントは使用しないのでコメントアウトしている。
rnd_search_cv.fit(X_train, # 訓練用データ
y_train, # 訓練用ラベル
epochs=EPOCHS, # エポック数
validation_data=(X_test, y_test), # Validationデータ
batch_size=BATCH_SIZE, # バッチサイズ
verbose=1, # 実行状況表示
callbacks=[
# cp, # チェックポイント
es] # 早期終了
)
結果の表示
検索で見つかったベストなモデルを選択して検証用データで汎化性能を評価するために plt でグラフ化している。
何回か実行して、最終的に以下のパラメータを採用することにした。
{'l2_rate': 0.0003955453968872585, 'learning_rate': 0.04968062858369911, 'n_hidden': 2, 'n_neurons': 48}
vali loss = 97.2074966430664
vali accuracy = 94.0851821899414
1 行目に最適なハイパーパラメータの検索結果が表示されている。
隠れ層の数が 2 なので Deep Neural Network とは言えない(一般的には隠れ層の数が 3 以上を Deep Neural Network と言う)が、とりあえず先に進める。
また検証データでのAge(年齢)の予測は以下の通り。
Hyperas での検索
最適なハイパーパラメータの検索を Hyperas でも同様に行ってみた。
尚、結論から先に書くと Hyperas の検索結果は採用しなかった。
というのも、お互いに何回か実行した所、RandomizedSearchCV での検索結果の方がスコアが良かったからだ。
恐らく特徴量やモデルによっても違いが出るので常に RandomizedSearchCV の方が良いハイパーパラメータを探してくるという訳では無いと思うが、今回のモデルの場合は上記のような結果になった。
titanic_dnn_age_optimize_hyperas.py
ソースコードは以下の通り。
# -*- coding: utf-8 -*-
"""
Created on Mon Oct 11 15:07:39 2021
Hyperasで最適なハイパーパラメーターを探す
Ageを'Pclass','Sex','Parch','SibSp'から推定するモデル
@author: Souichirou Kikuchi
"""
from sklearn.preprocessing import LabelEncoder # 文字列を数値に置き換える
from sklearn.preprocessing import StandardScaler # 標準化
from sklearn.preprocessing import MinMaxScaler # 正規化
from sklearn.model_selection import train_test_split # データを分割
from hyperas import optim
import pandas as pd
import matplotlib.pyplot as plt
from hyperopt import Trials, STATUS_OK, tpe
def data():
# GDRIVE = '/content/drive/MyDrive/M2B/Program/Kaggle/Titanic' # Google colab
GDRIVE = '.' # Local Python
train_csv = pd.read_csv(GDRIVE + '/csv/train.csv', index_col=False) # CSVの読み込み
test_csv = pd.read_csv(GDRIVE + '/csv/test.csv', index_col=False) # CSVの読み込み
tr_ts = pd.concat([train_csv, test_csv], ignore_index=True)
# Ageを推定可能な項目を取り出す
age_df = tr_ts[['Age', 'Name', 'Pclass', 'Sex', 'Parch', 'SibSp']]
# Nameから敬称(Mr.等を取り出す)
age_df['Name_ttl'] = age_df['Name'].str.extract(r'(\S+)\.', expand=False) # ピリオドで終わる文字列を取り出す
# 性別、敬称の数値化
for col in ['Sex', 'Name_ttl']:
le = LabelEncoder()
le.fit(age_df[col])
age_df[col] = le.transform(age_df[col])
# 正規化、標準化する項目のリスト
ms_ss_list = ['Pclass','Sex','Parch','SibSp', 'Name_ttl']
ms = MinMaxScaler(feature_range=(0, 1)) # データの正規化(0-1の範囲)
age_df.loc[:,ms_ss_list] = ms.fit_transform(age_df[ms_ss_list])
ss = StandardScaler() # データの標準化(正規分布に変換)
age_df.loc[:,ms_ss_list] = ss.fit_transform(age_df[ms_ss_list])
drop_list = ['Name']
for dl in drop_list:
age_df.drop(columns=[dl], inplace=True)
# Ageがnot nullとnullのデータセットに分割
age_nn = age_df[age_df.Age.notnull()].values
age_nl = age_df[age_df.Age.isnull()].values
# Not nullのデータを学習データに
train = age_nn[:, 1:]
age = age_nn[:, 0]
age_pr = age_nl[:, 1:] #年齢を予測したいデータセット
X_train, X_test, y_train, y_test = train_test_split(train,
age, # 学習する年齢データ
train_size = 0.8, # trainibg
test_size = 0.2, # Validation
random_state = 42, # random_stateはshuffle=Trueの時だけ使用される
shuffle = True) # shuffle=True(取り出す前にシャッフルする)
return X_train, X_test, y_train, y_test, age_pr
def build_model(X_train, X_test, y_train, y_test, age_pr):
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, InputLayer, Dropout
from tensorflow.keras import regularizers # 正則化
from tensorflow.keras import optimizers
from hyperas.distributions import choice, uniform
BATCH_SIZE = 10 # バッチサイズ
DROP_RATE = 0.2
REGULARIZATION = regularizers.l2({{uniform(0.0001, 0.01)}})
LOSS = 'mse'
METRICS = 'mse'
_, INPUT_DIM = X_train.shape # 入力列数
model = Sequential()
model.add(InputLayer(input_shape=[INPUT_DIM]))
for layer in range({{choice([1, 2, 3, 4, 5])}}):
model.add(Dense({{choice([1, 4, 8, 16, 32, 64, 128])}},
activation='sigmoid',
kernel_regularizer= REGULARIZATION))
model.add(Dropout(DROP_RATE))
model.add(Dense(1, activation='linear'))
optimizer = optimizers.Adam(lr={{uniform(0.003, 0.3)}})
model.compile(loss=LOSS, optimizer=optimizer, metrics=[METRICS]) # 精度
EPOCHS = 100 # エポック数
from tensorflow.keras.callbacks import EarlyStopping
# チェックポイント
# cp = ModelCheckpoint(GDRIVE + '/models/titanic_age_model_' + VERSION + '/age_predict_model.h5',
# save_best_only=True) # 最良モデルの保存
#過学習を防ぐための早期終了
es = EarlyStopping(monitor='val_loss',
patience=5, # このepoch数、性能が向上しなければストップ
restore_best_weights=True)
model.fit(X_train, # 訓練用データ
y_train, # 訓練用ラベル
epochs=EPOCHS, # エポック数
validation_data=(X_test, y_test),
batch_size=BATCH_SIZE, # バッチサイズ
verbose=1, # 実行状況表示
callbacks=[
# cp, # チェックポイント
es] # 早期終了
)
val_loss, val_acc = model.evaluate(X_test, y_test, verbose=0)
return {'loss': -val_acc, 'status': STATUS_OK, 'model': model}
#main
X_train, X_test, y_train, y_test, age_pr = data()
best_run, best_model = optim.minimize(model=build_model,
data=data,
algo=tpe.suggest,
max_evals=6, # ハイパーパラメータを探索する回数
# notebook_name ='drive/MyDrive/Colab Notebooks/titanic', # Colabの時は指定する
trials=Trials())
print(best_model.summary())
print(best_run)
# 未知のテストデータで学習済みモデルの汎化性能を評価
BATCH_SIZE = 10 # バッチサイズ
score = best_model.evaluate(X_test, y_test, batch_size=BATCH_SIZE)
print('vali loss =', score[0])
print('vali accuracy =', score[1])
pre = best_model.predict(age_pr)
print(pre)
plt.figure(figsize=(18, 9)) # 予測をグラフ化
plt.plot(pre, color='r', label='Predict')
plt.legend()
plt.show()
補足説明
プログラムソースコードは RandomizedSearchCV と重なる部分が多いので違う所だけ補足説明をする。
尚、データやモデル作成の部分を別モジュールにすると Hyperas ではうまく動作しなかった(やり方はあるのだろうが自分は見つけられなかった)
Hyperas では動的にモデルのモジュールを作成する様で(temp_model.py というファイルが作成されていた)その関係か?一つのモジュールでコードを書いたほうが良かったのでモジュールは分けていない。
L2正則化率
検索したいハイパーパラメータを二重括弧 “{{” “}}” で囲む。
uniform で 0.0001 から 0.01 の範囲で最適なL2正則化率を検索する。
REGULARIZATION = regularizers.l2({{uniform(0.0001, 0.01)}})
隠れ層とニューロン数
隠れ層とニューロン数は以下のコードで検索している。
choice でリストの中から最適なハイパーパラメータを検索する。
for layer in range({{choice([1, 2, 3, 4, 5])}}):
model.add(Dense({{choice([1, 4, 8, 16, 32, 64, 128])}},
activation='sigmoid',
kernel_regularizer= REGULARIZATION))
notebook_name
ローカルで実行する際には不要なのでコメントアウトしているが、Google Colab で実行する際には notebook_name の指定が必要になる。
Path も含めたノートブックの名前(拡張子の ipynb は必要ない)を指定する。
# notebook_name ='drive/MyDrive/Colab Notebooks/titanic/titanic', # Colabの時は指定する
以上で Hyperas による最適ハイパーパラメータの検索は終了。
年齢予測モデルの学習
次に最適なハイパーパラメータが決まったらその数値を元に年齢予測モデルで学習する。
尚、自分の場合は最適なハイパーパラメータを検索するプログラムと実際のモデルを作成するプログラムを分けたが1つのプログラムで行っても良いと思う。
titanic_dnn_age.py
age_data.pyとage_data.py は同じものを使用してモデルを学習する部分だけを変更する。
# -*- coding: utf-8 -*-
"""
Created on Sat Oct 9 08:18:51 2021
Ageを'Pclass','Sex','Parch','SibSp'から推定するモデルを作成する
・RandomizedSearchCV及びHyperasで最適なパラメータを探した
@author: Souichirou Kikuchi
"""
VERSION = 'v005'
# GDRIVE = '/content/drive/MyDrive/M2B/Program/Kaggle/Titanic' # Google colab
GDRIVE = '.' # Local Python
import sys # 自作モジュールへのPathを追加する(colabで必要)
ROOT_PATH = GDRIVE
sys.path.append(ROOT_PATH)
from age_data import a_data # データ前処理
from age_model import a_build_model # モデル作成
from tensorflow.keras import models
import matplotlib.pyplot as plt
METRICS = 'mse'
# main
X_train, X_test, y_train, y_test, age_pr = a_data(drive = GDRIVE, r_state = False) # データ前処理
BATCH_SIZE = 10 # バッチサイズ
EPOCHS = 100 # エポック数
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
# チェックポイント
cp = ModelCheckpoint(GDRIVE + '/models/titanic_age_model_' + VERSION + '/age_predict_model.h5',
save_best_only=True) # 最良モデルの保存
#過学習を防ぐための早期終了
es = EarlyStopping(monitor='val_loss',
patience=5, # このepoch数、性能が向上しなければストップ
restore_best_weights=True)
_, INPUT_DIM = X_train.shape # 入力列数
model = a_build_model(n_hidden=2, # RandomizedSearchCVで検索したハイパーパラメータを指定する
n_neurons=48,
learning_rate=0.04968,
l2_rate=0.0003955,
input_shape=[INPUT_DIM])
# 学習する
hist =model.fit(X_train, # 訓練用データ
y_train, # 訓練用ラベル
epochs=EPOCHS, # エポック数
validation_data=(X_test, y_test), # Validationデータ
batch_size=BATCH_SIZE, # バッチサイズ
verbose=1, # 実行状況表示
callbacks=[
cp, # チェックポイント
es] # 早期終了
)
# 学習結果(損失=交差エントロピー)のグラフを描画
plt.figure()
train_loss = hist.history['loss']
valid_loss = hist.history['val_loss']
epochs = len(train_loss)
plt.plot(range(epochs), train_loss, marker='.', label='loss (training data)')
plt.plot(range(epochs), valid_loss, marker='.', label='loss (validation data)')
plt.legend(loc='best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss (cross entropy)')
# 評価関数(正解率)のグラフを描画
plt.figure()
train_mae = hist.history[METRICS]
valid_mae = hist.history['val_' + METRICS]
epochs = len(train_mae)
plt.plot(range(epochs), train_mae, marker='.', label='accuracy (training data)')
plt.plot(range(epochs), valid_mae, marker='.', label='accuracy (validation data)')
plt.legend(loc='best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()
# 最良モデルへのロールバック
model = models.load_model(GDRIVE + '/models/titanic_age_model_' + VERSION + '/age_predict_model.h5')
# 未知のテストデータで学習済みモデルの汎化性能を評価
score = model.evaluate(X_test, y_test, batch_size=BATCH_SIZE)
print('vali loss =', score[0])
print('vali accuracy =', score[1])
pre = model.predict(age_pr)
print(pre)
plt.figure(figsize=(18, 9)) # 予測をグラフ化
plt.plot(pre, color='r', label='Predict')
plt.legend()
plt.show()
VERSION
複数のモデルを作成して以前のモデルを残して置きたいこともあるので、モデルのバージョン管理を行う定数を指定して保存するフォルダーを分けるようにした。
VERSION = 'v005'
random_state
データの前処理を行う際に random_state に固定値を設定しない(r_state = False)ように呼び出す。
実行毎に異なる分割(学習と研修データ)をする。
この状態で何回か実行を行い、検証用データで一番良いスコアが出たモデルを最終モデルとした。
X_train, X_test, y_train, y_test, age_pr = a_data(drive = GDRIVE, r_state = False) # データ前処理
チェックポイント
Epoch 数に大きな値を設定して val_loss(検証データの損失)が向上しなくなった時に早急終了するよにしている。
このため、最後のEpoch まで学習したモデルが最良とは限らないので、チェックポイントを取得して最良モデルの保存を行っている。
# チェックポイント
cp = ModelCheckpoint(GDRIVE + '/models/titanic_age_model_' + VERSION + '/age_predict_model.h5',
save_best_only=True) # 最良モデルの保存
ハイパーパラメータ
前述の RandomizedSearchCV で検索した最適なハイパーパラメータを指定した。
model = build_model(n_hidden=2,
n_neurons=48,
learning_rate=0.04968,
l2_rate=0.0003955,
input_shape=[INPUT_DIM])
最良モデルのロールバック
最良モデルへのロールバックを行い、その後、検証用データで汎化性能を評価した。
># 最良モデルへのロールバック
model = models.load_model(GDRIVE + '/models/titanic_age_model_' + VERSION + '/age_predict_model.h5')
学習結果
何回か実行して以下の結果を最終モデルとした。
損失関数の推移(学習データ)
評価用データでの損失と正解率
評価用(Validation)データでの損失関数と評価関数(正解率)の結果。
mean_squared_error: 103.2972
vali loss = 104.73005676269531
vali accuracy = 103.29716491699219
検証価用データでの年齢予測
検証用データでの年齢予測をグラフ化したもの。
生存率を予測する
年齢を予測する Neural Network Model ができたので次は生存率を予測する。
尚、生存率の予測は Age(年齢)の欠損値予測と同様に、
- データの前処理
- RandomizedSearchCV による最適なハイパーパラメータの検索
- Hyperas による最適なハイパーパラメータの検索
- 上記で検索したハイパーパラメータでモデルの学習
- 生存率の予測
の順番で行った。
RandomizedSearchCVで最適なハイパーパラメータの検索
main_data.py
データの前処理を行う関数を別モジュールにしている。
# -*- coding: utf-8 -*-
"""
Created on Mon Nov 1 21:24:54 2021
・生存率予測・データ前処理
@author: Souichirou Kikuchi
"""
from age_data import a_data # Ageデータ前処理
def m_data(drive, r_state):
import re # 正規表現
import pandas as pd
from sklearn.preprocessing import LabelEncoder # 文字列を数値に置き換える
from sklearn.preprocessing import StandardScaler # 標準化
from sklearn.preprocessing import MinMaxScaler # 正規化
from tensorflow.keras import models
from sklearn.model_selection import train_test_split # データを分割
def ticket_alpha(df): # Ticketアルファベット
s = df['Ticket']
if s.isnumeric(): # 数字
result = 'ZZZ' # 数字のみのチケット
else:
rtn = re.findall(r'\w+', s) # アルファベット、アンダーバー、数字(複数の時は配列)
rslt = ''
if len(rtn) > 1:
for i in range(len(rtn)-1): # 最後の配列は数字なので入れない
rslt += rtn[i]
else:
rslt = rtn[0] # 英字だけのTicket
result = rslt
return result
def ticket_num(df): # Ticket数字
s = df['Ticket']
if s.isnumeric(): # 数字
result = float(s)
else:
rtn = re.findall(r'\d+', s) # 数字部分を取り出し(複数の時は配列)
if len(rtn) > 0:
result = float(rtn[len(rtn)-1]) # 複数取り出した時は最後を使う
else:
result = float(999999) # 英字のみのTicketは999999をセット
return result
train_csv = pd.read_csv(drive + '/csv/train.csv', index_col=False) # CSVの読み込み
test_csv = pd.read_csv(drive + '/csv/test.csv', index_col=False) # CSVの読み込み
tr_ts = pd.concat([train_csv, test_csv], ignore_index=True)
_, _, _, _, age_pr = a_data(drive = drive, r_state = False) # データ前処理(age_prだけ受け取る))
model = models.load_model(drive + '/models/titanic_age_model_v005/age_predict_model.h5') # Age予測モデル
age_pre = model.predict(age_pr)
tr_ts.loc[(tr_ts.Age.isnull()), 'Age'] = age_pre
# FareがNANのデータを男女、Pclass別の平均で穴埋め
sx_list = ['male', 'female'] # Sex
pc_list = [1, 2, 3] # Pclass
for sx in sx_list:
for pl in pc_list:
tr_ts.loc[(tr_ts['Sex'].values == sx) & (tr_ts['Pclass'].values == pl) & (tr_ts['Fare'].isnull()), 'Fare'] = tr_ts.query('Sex == "' + sx + '" & Pclass == ' + str(pl))['Fare'].mean()
# Embarked(乗船場所)は最頻値で置き換え
tr_ts.fillna({'Embarked': tr_ts['Embarked'].mode()[0]}, inplace=True)
# Ticketのアルファベット部・整形
tr_ts['Ticket_alpha'] = tr_ts.apply(ticket_alpha, axis=1)
# Ticketの数字部・整形
tr_ts['Ticket_num'] = tr_ts.apply(ticket_num, axis=1)
# Cabin整形 先頭の1文字
tr_ts.fillna({'Cabin': 'Z'}, inplace=True) # NanはZ埋め
tr_ts['Cabin_alpha'] = tr_ts['Cabin'].str[:1]
# Nameから敬称(Mr.等を取り出す)
tr_ts['Name_ttl'] = tr_ts['Name'].str.extract(r'(\S+)\.', expand=False) # ピリオドで終わる文字列を取り出す
# Nameから姓を取り出す
tr_ts['Family_name'] = tr_ts['Name'].str.extract(r'(\S+)\,', expand=False) # カンマの前の文字列を取り出す
# 性別、乗船場所、チケットアルファベット、キャビン(最初の1文字)、名前の敬称、姓を数値化
for col in ['Sex', 'Embarked', 'Ticket_alpha', 'Cabin_alpha', 'Name_ttl', 'Family_name']:
le = LabelEncoder()
le.fit(tr_ts[col])
tr_ts[col] = le.transform(tr_ts[col])
# Ticket再整形(アルファベット部と数字部分の結合)
tr_ts['Ticket_ttl'] = (tr_ts['Ticket_alpha'] * 10**6) + tr_ts['Ticket_num']
# 姓とFare(料金)を結合・家族を特定する
tr_ts['Family_fare'] = tr_ts['Family_name'] * 10**3 + tr_ts['Fare']
# Fare(料金)をSibSp+Parch + 1で割る
tr_ts['Fare'] = tr_ts['Fare'] / (tr_ts['SibSp'] + tr_ts['Parch'] + 1)
# SibSp+ParchでFamily_num(家族数)項目を作る
tr_ts['Family_num'] = tr_ts['SibSp'] + tr_ts['Parch']
# その他使用しないカラムを削除する
drop_list = ['Name', 'Ticket', 'Ticket_alpha', 'Ticket_num', 'Cabin', 'SibSp', 'Parch', 'Family_name']
for dl in drop_list:
tr_ts.drop(columns=[dl], inplace=True)
# 正規化、標準化する項目のリスト
ms_ss_list = ['Pclass','Sex','Age','Family_num','Fare','Embarked','Cabin_alpha','Name_ttl','Family_fare','Ticket_ttl']
ms = MinMaxScaler(feature_range=(0, 1)) # データの正規化(0-1の範囲)
tr_ts.loc[:,ms_ss_list] = ms.fit_transform(tr_ts[ms_ss_list])
ss = StandardScaler() # データの標準化(正規分布に変換)
tr_ts.loc[:,ms_ss_list] = ss.fit_transform(tr_ts[ms_ss_list])
# trainとtestの分割
train = tr_ts[:891].copy()
test = tr_ts[891:].copy()
survived = train.Survived.values # 生存者カラムを取り出す
train.drop(columns=['Survived'], inplace=True) # 生存者カラムを削除する
test.drop(columns=['Survived'], inplace=True)
test.to_csv(drive + '/csv/test_pid.csv', header=True, index=False) # 予測時のPassengerIdの為のCSVを保存する
train.drop(columns=['PassengerId'], inplace=True)
test.drop(columns=['PassengerId'], inplace=True)
test.to_csv(drive + '/csv/test_predict.csv', header=True, index=False) # 予測用CSVを保存する
if r_state == True:
X_train, X_test, y_train, y_test = train_test_split(train,
survived, # 生存者データ
train_size = 0.8, # trainibg
test_size = 0.2, # Validation
random_state = 42, # 毎回固定(モデルの比較時など)
shuffle = True) # 取り出す前にシャッフルする
else: # random_state未指定
X_train, X_test, y_train, y_test = train_test_split(train,
survived, # 生存者データ
train_size = 0.8, # trainibg
test_size = 0.2, # Validation
shuffle = True) # 取り出す前にシャッフルする
return X_train, X_test, y_train, y_test, test
Age の欠損値を埋める
Age データの前処理のモジュールを呼び出して予測したい Age が欠損値の予測用データを取り出して、先程学習&保存した Age Neural Network Model で推論して欠損値を埋める。
_, _, _, _, age_pr = a_data(drive = drive, r_state = False) # データ前処理(age_prだけ受け取る))
model = models.load_model(drive + '/models/titanic_age_model_v005/age_predict_model.h5') # Age予測モデル
age_pre = model.predict(age_pr)
tr_ts.loc[(tr_ts.Age.isnull()), 'Age'] = age_pre
Fare(旅客運賃)
欠損値があったので、Sex(性別)、Pclass(チケットクラス)の平均値から穴埋めした。
# FareがNANのデータを男女、Pclass別の平均で穴埋め
sx_list = ['male', 'female'] # Sex
pc_list = [1, 2, 3] # Pclass
for sx in sx_list:
for pl in pc_list:
tr_ts.loc[(tr_ts['Sex'].values == sx) & (tr_ts['Pclass'].values == pl) & (tr_ts['Fare'].isnull()), 'Fare'] = tr_ts.query('Sex == "' + sx + '" & Pclass == ' + str(pl))['Fare'].mean()
Embarked(乗船場所)
乗船場所も欠損値があったので最頻値で穴埋めした。
# Embarked(乗船場所)は最頻値で置き換え
tr_ts.fillna({'Embarked': tr_ts['Embarked'].mode()[0]}, inplace=True)
Ticket
チケットは 3 種類に分類されたのでそれぞれ前処理を行った。
英数字+数字のチケット | 先頭の英数字部分と数字部分を別々に取り出す 尚、この際の先頭の英数字だが「アルファベット、アンダーバー、数字」以外は無視している。
入力値にゆらぎがあったので同じ値として扱えるように余分な文字は無視した。 また最後の数字以前の英数字を全て結合して英数字として取り出した。
|
数字のみのチケット | 英数字部分に Dummy の値、”ZZZ” をセットして数字部分を取り出す |
英数字のみのチケット | 英数字部分を取り出して、数字にDummyの値 “999999” をセットする |
def ticket_alpha(df): # Ticketアルファベット
s = df['Ticket']
if s.isnumeric(): # 数字
result = 'ZZZ' # 数字のみのチケット
else:
rtn = re.findall(r'\w+', s) # アルファベット、アンダーバー、数字(複数の時は配列)
rslt = ''
if len(rtn) > 1:
for i in range(len(rtn)-1): # 最後の配列は数字なので入れない
rslt += rtn[i]
else:
rslt = rtn[0] # 英字だけのTicket
result = rslt
return result
def ticket_num(df): # Ticket数字
s = df['Ticket']
if s.isnumeric(): # 数字
result = float(s)
else:
rtn = re.findall(r'\d+', s) # 数字部分を取り出し(複数の時は配列)
if len(rtn) > 0:
result = float(rtn[len(rtn)-1]) # 複数取り出した時は最後を使う
else:
result = float(999999) # 英字のみのTicketは999999をセット
return result
上記の処理で全てのチケットを英数字部分+数字部分に変換した後、英数字部分を LabelEncoder() で数値に変換して更に数字部分と結合する。
つまり英数字も含めて全体で大きな数値に変換した。
チケットの数字は最大で 6桁だったので、英数字を数値化した値に 1,000,000 を乗算して一つの数値項目にしている。
# 性別、乗船場所、チケットアルファベット、キャビン(最初の1文字)、名前の敬称、姓を数値化
for col in ['Sex', 'Embarked', 'Ticket_alpha', 'Cabin_alpha', 'Name_ttl', 'Family_name']:
le = LabelEncoder()
le.fit(tr_ts[col])
tr_ts[col] = le.transform(tr_ts[col])
# Ticket再整形(アルファベット部と数字部分の結合)
tr_ts['Ticket_ttl'] = (tr_ts['Ticket_alpha'] * 10**6) + tr_ts['Ticket_num']
Cabin
Cabin は入力されていることが少ないので無視しようとも思ったのだが、Cabin の最初の一文字目のアルファベットが若い方が生存率が高い事が分かったので特徴量として加えた。
客室の上位層から順番にA、B、C と振られていて上位層の方が助かりやすかったのだろう。
Nan は “Z ” で穴埋めして最初の一文字目を取り出して LabelEncoder() で数値化したがAge(年齢)と同様にPclass(チケットクラス)や Fare(料金)から予測しても良いと思う。
# Cabin整形 先頭の1文字
tr_ts.fillna({'Cabin': 'Z'}, inplace=True) # NanはZ埋め
tr_ts['Cabin_alpha'] = tr_ts['Cabin'].str[:1]
Name欄の敬称
Name 欄に入っている敬称(Mr.とかMis.など)を特徴量として使用した。
名前欄で .(ピリオド)で終了する文字列を取り出して LabelEncoder() で数値化した。
# Nameから敬称(Mr.等を取り出す)
tr_ts['Name_ttl'] = tr_ts['Name'].str.extract(r'(\S+)\.', expand=False) # ピリオドで終わる文字列を取り出す
Name欄の家族名(姓)
家族で乗船している場合は家族で運命を共にする可能性が高いとの仮説により、家族をグルーピングしたかった。
データを眺めてみると同一家族は、
- Name欄の家族名(姓)が同じ
- Fare(料金)が同じ(一緒に支払いをしている)
の特徴があったので Name 欄から ,(カンマ)の前の文字列を取り出してLabelEncoder() で数値化した。
# Nameから姓を取り出す
tr_ts['Family_name'] = tr_ts['Name'].str.extract(r'(\S+)\,', expand=False) # カンマの前の文字列を取り出す
数値化した後、1,000 を乗算して Fare(料金)と合算した。
これで同一家族は同じ数値になるはずなので特徴量として使えるのではと考えた。
# 姓とFare(料金)を結合・家族を特定する
tr_ts['Family_fare'] = tr_ts['Family_name'] * 10**3 + tr_ts['Fare']
Fare(料金)の加工
Fare(料金)は家族の分も合算した金額のように見える。
家族と思われるデータをGoogleスプレッドシートで見ていた所、同じ数字が入っていた。
このままでも良かったのかも知れないが Fare を家族人数(SibSp+Parch)で割って一人あたりの概算料金に変換してみた。
# Fare(料金)をSibSp+Parch + 1で割る
tr_ts['Fare'] = tr_ts['Fare'] / (tr_ts['SibSp'] + tr_ts['Parch'] + 1)
Fare(料金)は高い方が生存率が高いとの分析結果があった。
家族分の料金を払っている = 高い料金を払える家族
との考え方も出来るのでもしかしたらこの値は家族数で割る必要は無いのかも知れない。
家族数
家族数も特徴量に加えた。
# SibSp+ParchでFamily_num(家族数)項目を作る
tr_ts['Family_num'] = tr_ts['SibSp'] + tr_ts['Parch']
main_model.py
モデルを作成する関数を別モジュールにしている。
# -*- coding: utf-8 -*-
"""
Created on Mon Nov 1 21:33:39 2021
・生存率予測・モデル作成
@author: Souichirou Kikuchi
"""
# モデル作成
def m_build_model(n_hidden, n_neurons, learning_rate, l2_rate, input_dim=10):
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Flatten
from tensorflow.keras import regularizers # 正則化
from tensorflow.keras import optimizers
REGULARIZATION = regularizers.l2(l2_rate) # 正則化:L2、正則化率
LOSS = 'binary_crossentropy' # 損失関数:二値分類用の交差エントロピー
METRICS = 'binary_accuracy' # 評価関数:2クラス分類の正解率
DROP_RATE = 0.2
model = Sequential()
model.add(Flatten(input_shape=(1, input_dim),# 入力層:入力データのフラット化(Flatten)
name='flatten_input1'))
for layer in range(n_hidden):
model.add(Dense(n_neurons,
# activation='elu',
activation='relu',
kernel_regularizer= REGULARIZATION,
kernel_initializer='he_normal'))
model.add(Dropout(DROP_RATE))
model.add(Dense(1, activation='sigmoid')) # 2値分類
optimizer = optimizers.Adam(lr=learning_rate)
model.compile(loss=LOSS,
optimizer=optimizer,
metrics=[METRICS]) # 精度
return model
損失と評価関数
損失関数は binary_crossentropy(交差エントロピー誤差:2クラス分類)を指定した。
2 クラス分類でも指数関数的に変化を表す事が可能で 0 ~ 1 の予測値でも 1 に近い 0 等、グラデーションを表せる。
評価関数は binary_accuracy(2クラス分類の正解率)を指定した。
LOSS = 'binary_crossentropy' # 損失関数:二値分類用の交差エントロピー
METRICS = 'binary_accuracy' # 評価関数:2クラス分類の正解率
活性化関数と初期化
活性化関数は “selu” と “relu” を試して “relu” を採用した。
またその際の初期化パラメータは “he_normal” を指定している。(未指定だとデフォルトのGlorotが指定される)
書籍「scikit-learn、Keras、TensorFlowによる実践機械学習 第2版」の初期化パラメータと活性化関数との関係を参考にした。
Glorot(デフォルト) | なし、tanh、ロジスティック、ソフトマックス |
He | ReLuとその変種 |
LeCun | SELU |
model.add(Dense(n_neurons,
activation='relu',
kernel_regularizer= REGULARIZATION,
kernel_initializer='he_normal'))
titanic_dnn_main_optimize.py
最適なハイパーパラメータを検索するメイン処理。
main_data モジュールと main_model モジュールを使って RandomizedSearchCV でパラメータ検索を行う。
# -*- coding: utf-8 -*-
"""
Created on Tue Sep 28 14:59:31 2021
Kagle タイタニック生存者予測(学習)
RandomizedSearchCVでハイパーパラメータの検索
@author: Souichirou Kikuchi
"""
# GDRIVE = '/content/drive/MyDrive/M2B/Program/Kaggle/Titanic' # Google colab
GDRIVE = '.' # Local Python
import sys # 自作モジュールへのPathを追加する(colabで必要)
ROOT_PATH = GDRIVE
sys.path.append(ROOT_PATH)
from main_data import m_data # データ前処理
from main_model import m_build_model # モデル作成
import matplotlib.pyplot as plt
# main
X_train, X_test, y_train, y_test, test = m_data(drive = GDRIVE, r_state = True) # データ前処理
_, INPUT_DIM = X_train.shape # 入力列数
BATCH_SIZE = 10 # バッチサイズ
EPOCHS = 100 # エポック数
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.wrappers.scikit_learn import KerasRegressor
# チェックポイント
#cp = ModelCheckpoint(GDRIVE + '/models/titanic_model_' + VERSION + '/main_predict_model.h5',
# save_best_only=True) # 最良モデルの保存
#過学習を防ぐための早期終了
es = EarlyStopping(monitor='val_loss', # Validationの損失を基準にする
patience=5, # このepoch数、性能が向上しなければストップ
restore_best_weights=True) # 最適な重みを復元する
keras_reg = KerasRegressor(m_build_model)
from scipy.stats import reciprocal
from sklearn.model_selection import RandomizedSearchCV
import numpy as np
param_distribs = {
'n_hidden': [1, 2, 3, 4, 5],
'n_neurons': np.arange(8, 64),
'learning_rate': reciprocal(3e-4, 3e-1),
'l2_rate': reciprocal(3e-4, 3e-1)
}
rnd_search_cv = RandomizedSearchCV(estimator = keras_reg, # 推定するオブジェクト
param_distributions = param_distribs, # パラメータ
n_iter = 20, # サンプリングするパラメータの数。大きくすると解の質は良くなるが時間が掛かる
cv=5) # 交差検証の数
rnd_search_cv.fit(X_train, # 訓練用データ
y_train, # 訓練用ラベル
epochs=EPOCHS, # エポック数
# validation_split=0.2, # 精度検証用の割合
validation_data=(X_test, y_test), # Validationデータ
batch_size=BATCH_SIZE, # バッチサイズ
verbose=1, # 実行状況表示
callbacks=[
# cp, # チェックポイント
es] # 早期終了
)
print('best_params= ', rnd_search_cv.best_params_)
print('best_score= ', rnd_search_cv.best_score_)
model = rnd_search_cv.best_estimator_.model # ベストなモデルを選択
model.summary() # テキストで出力
# 未知のテストデータで学習済みモデルの汎化性能を評価
score = model.evaluate(X_test, y_test, batch_size=BATCH_SIZE)
print('vali loss =', score[0])
print('vali accuracy =', score[1])
pre = model.predict(test)
# print(pre)
plt.figure(figsize=(18, 9)) # 予測をグラフ化
plt.plot(pre, color='r', label='Predict')
plt.legend()
plt.show()
main
main 部分については概ね Age の場合と同じなので補足説明は省略する。
検索結果
RandomizedSearchCV での最適なハイパーパラメータの検索結果は以下の通り。
隠れ層が 1 なのが何故かちょっと寂しいが、このネットワークが最適とのことなので信用することにする。
また Hyperas でも同様に最適なハイパーパラメータの検索を行ったのだが、こちらの方がスコアが良かったので、このハイパーパラメータを採用することとする。
best_params= {'l2_rate': 0.0009167466210143319, 'learning_rate': 0.014022367002986662, 'n_hidden': 1, 'n_neurons': 46}
best_score= -0.48287753264109295
vali loss = 0.4154411256313324
vali accuracy = 0.8547486066818237
生存率の学習モデル
RandomizedSearchCV で検索した最適なハイパーパラメータを指定して生存率の学習モデルを作成する。
尚、Age の時も書いたが最適ハイパーパラメータの検索と学習はひとつのプログラムでも実行可能だとは思うが、random_state をコメントアウトしてチェックポイント機能を使って何回も実行して良さげなモデルを探したかったのでプログラムを分けた。
titanic_dnn_main.py
生存率の学習モデルの作成。
# -*- coding: utf-8 -*-
"""
Created on Tue Sep 28 14:59:31 2021
Kagle タイタニック生存者予測(学習)
@author: Souichirou Kikuchi
"""
VERSION = 'v021'
# GDRIVE = '/content/drive/MyDrive/M2B/Program/Kaggle/Titanic' # Google colab
GDRIVE = '.' # Local Python
import sys # 自作モジュールへのPathを追加する(colabで必要)
ROOT_PATH = GDRIVE
sys.path.append(ROOT_PATH)
from main_data import m_data # データ前処理
from main_model import m_build_model # モデル作成
from tensorflow import keras
import matplotlib.pyplot as plt
# main
X_train, X_test, y_train, y_test, test = m_data(drive = GDRIVE, r_state = False) # データ前処理
_, INPUT_DIM = X_train.shape # 入力列数
BATCH_SIZE = 10 # バッチサイズ
EPOCHS = 100 # エポック数
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
# チェックポイント
cp = ModelCheckpoint(GDRIVE + '/models/titanic_model_' + VERSION + '/main_predict_model.h5',
save_best_only=True) # 最良モデルの保存
#過学習を防ぐための早期終了
es = EarlyStopping(monitor='val_loss',
patience=3, # このepoch数、性能が向上しなければストップ
restore_best_weights=True) # 最適な重みを保存する
model = m_build_model(n_hidden=1, # RandomizedSearchCVで検索したハイパーパラメータを指定する
n_neurons=46,
learning_rate=0.014022367002986662,
l2_rate=0.0009167466210143319,
input_dim=INPUT_DIM)
model.summary() # テキストで出力
hist = model.fit(X_train, # 訓練用データ
y_train, # 訓練用ラベル
epochs=EPOCHS, # エポック数
validation_data=(X_test, y_test), # Validationデータ
batch_size = BATCH_SIZE, # バッチサイズ
verbose = 1, # 実行状況表示
callbacks = [
cp, # チェックポイント
es] # 早期終了
)
# 学習結果(損失=交差エントロピー)のグラフを描画
plt.figure()
train_loss = hist.history['loss']
valid_loss = hist.history['val_loss']
epochs = len(train_loss)
plt.plot(range(epochs), train_loss, marker='.', label='loss (training data)')
plt.plot(range(epochs), valid_loss, marker='.', label='loss (validation data)')
plt.legend(loc='best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss (cross entropy)')
# 評価関数(正解率)のグラフを描画
METRICS = 'binary_accuracy' # 評価関数:2クラス分類の正解率
plt.figure()
train_mae = hist.history[METRICS]
valid_mae = hist.history['val_' + METRICS]
epochs = len(train_mae)
plt.plot(range(epochs), train_mae, marker='.', label='accuracy (training data)')
plt.plot(range(epochs), valid_mae, marker='.', label='accuracy (validation data)')
plt.legend(loc='best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()
# 未知のテストデータで学習済みモデルの汎化性能を評価
model = keras.models.load_model(GDRIVE + '/models/titanic_model_' + VERSION + '/main_predict_model.h5') # 最良モデルを読み直す
score = model.evaluate(X_test, y_test, batch_size=BATCH_SIZE)
print('loss =', score[0])
print('accuracy =', score[1])
実行結果
損失関数の推移。
評価関数(正解率)の推移。
評価データでの正解率。
binary_accuracy: 0.8212
vali loss = 0.4912284016609192
vali accuracy = 0.8212290406227112
学習に使用していない評価データ(train.csvの一部)で82%以上の正解率が出たので test.csv でも82% 近い正解率が出るのかというと、実はそうでもない。
評価データで 90% 近い正解率が出た学習モデルもあったが、逆に test.csv では低い正解率になってしまった。
恐らく過学習が原因だと思うが、評価データの正解率よりも損失関数のグラフで正しく学習が進んでいる曲線が現れているかの方が重要なのだと思う。
提出用データの作成
以下のプログラムで提出用データの作成を行った。
titanic_dnn_main_predict.py
# -*- coding: utf-8 -*-
"""
Created on Thu Sep 30 16:50:54 2021
Kagle タイタニック生存者予測(推論)
・csv/test_predict.csv より予測を行う
@author: Souichirou Kikuchi
"""
VERSION = 'v018'
import pandas as pd
import numpy as np
from tensorflow import keras
import matplotlib.pyplot as plt
# GDRIVE = '/content/drive/MyDrive/M2B/Program/Kaggle/Titanic' # Google colab
GDRIVE = '.' # Local Python
test_predict = pd.read_csv(GDRIVE + '/csv/test_predict.csv', encoding='shift-jis') # 予測用CSV(PassengerId無し)
test_pid = pd.read_csv(GDRIVE + '/csv/test_pid.csv', encoding='shift-jis') # PassengerId取得用
model = keras.models.load_model(GDRIVE + '/models/titanic_model_'+ VERSION + '/main_predict_model.h5')
pre = model.predict(test_predict)
plt.figure(figsize=(18, 9)) # 予測をグラフ化
plt.plot(pre, color='r', label='Predict') # 予測
plt.legend()
plt.show()
ids = test_pid['PassengerId']
pred = np.int32(pre >= 0.5)
pred_ = np.ravel(pred)
output = pd.DataFrame({ 'PassengerId' : ids, 'Survived': pred_ })
output.to_csv(GDRIVE + '/output/DNN_' + VERSION + '.csv', index = False)
提出結果
結果は 0 ~ 1 の浮動小数点で出力されるので 0.5 をしきい値として 0 と 1 に分割した。
上記のコードで kaggle で提出を行った所、正解率 0.79665 だった。
まだやれることは色々とありそうだが、ひとまず Neural Network での生存率の予測は終了する。
つづく
長くなったので一旦終了とする。
次回はランダムフォレストでの予測とする。
この記事が何処かで誰かの役に立つことを願っている。
尚、当記事中の商品へのリンクはAmazonアソシエイトへのリンクが含まれています。Amazonのアソシエイトとして、当メディアは適格販売により収入を得ていますのでご了承ください。
最近のコメント