これ何の記事?
SIGNATEのAIビギナー向けコンペ【健診データによる肝疾患判定】に参加し、
順位:59/255位で中級者に昇格できたので、その時の知見を残す。
ニューラルネットや決定木モデル、それらのアンサンブルを試したけど、
結局LightGBMが強かったという話。
実際に動かせるように、過保護なほど細かく書いています。
参加コンペ:SIGNATE「健診データによる肝疾患判定」(リンク)
本記事での最終結果
まず結果の最高精度と手法について箇条書きでまとめる。
【運営にアップロードしたものと評価方法】
提出物:テストデータの番号とそれらに対する0~1の予測値(病気である確率)のCSVファイル
運営側にて、提出物と正解データを比較し、AUCで評価
※AUCとは?:評価関数の一つ、AUCが大きいほど優れた予測といえる。(AUCの詳細は下記リンク先がわかりやすい)
【評価指標】ROC 曲線と AUC についてわかりやすく解説してみた
【手法と精度】
・最高精度AUC :0.9226468(昇格ラインぎりぎり💦)
・最終使用モデル:LightGBM単体
・データの前処理:結局何もしないのが、今回の私の手法ではよかった
・その他取り組み:交差検証(KFold)での評価、Optunaでハイパラ自動調整
さて実装の話に移っていくが、ここからは知ってるよ!! ( ゚Д゚)
...って人はちゃっちゃかコードをgithubにて共有しておく
(ついでに試しに書いたXGBoostとTabnetのコードも共有)
Github-SIGNATE「健診データから肝疾患の予測」notebook
前準備(実際に動かしたい人向け)
Google ColabratoryとGoogleDriveで実装しているため、Googleアカウントを持っていれば誰でも実装できる!(*´ω`)
データのダウンロード
今回のテーマは練習問題のデータを初心者専用コンペに設定されていたので、コンペが終わった後も下記リンク先から同じデータをダウンロードして試すことが出来る。
なので試したければ、下記リンク先の「データ」からtrain.csvとtest.csvをダウンロードする。
【練習問題】健診データによる肝疾患判定
ダウンロードしたデータはGoogleDriveへ保存!
私は『/signate/健診データによる肝疾患判定/dataset』のフォルダを作ってtrain.csvとtest.csvの格納した(下記参照)
以降は上記と同じフォルダ構成と想定して記載していく。
格納先ファイルパスが異なる場合は適宜書き換えてほしい。
実装環境とGoogleDriveのマウント準備
Google Colabratory
└「ファイル」ー「ノートブックを新規作成」で新たに作成
作成したノートブックに下記コードを書いてGoogleDriveをマウントする!
from google.colab import drive
drive.mount('/content/drive')
%cd /content/drive/ 'My Drive'/signate/健診データによる肝疾患判定
Shift+Enterで実行!Googleアカウントを認証して、GoogleDriveをマウント完了!
お題データ読込と傾向の確認
なにはともあれ、、まずはお題データのカラムや意味、傾向を確認!
問題の理解、カラムや意味の確認
今回の問題は、
特徴変数:[Age、Gender、T_Bil、D_Bil、ALP、ALT_GPT、AST_GOT、TP、Alb、AG_ratio]
目的変数:[disease]
である。
また、目的変数 diseaseが 0 or 1または0 ~ 1(病気である確率がどのくらいか)を求める問題であることがわかった。
特徴変数の各意味はSIGNATEのデータ説明を参照。
よくわかんねぇなぁ・・・ってのは、ネット検索で調べて見ましょう。
データの傾向を確認
データ形式確認とカテゴリ値の数字化
DataFramedで読み込んで、head()で表示してみる
import pandas as pd
# 学習用csvデータ確認
df = pd.read_table('./dataset/train.csv', sep=',')
df.columns = ['Age', 'Gender', 'T_Bil', 'D_Bil', 'ALP', 'ALT_GPT', 'AST_GOT', 'TP', 'Alb', 'AG_ratio', 'disease']
df.head()
ほぅ!「Gender」は数値ではなくMaleかFamaleのカテゴライズ値なのか!
ということが分かったので、AI君が扱えるように数値型に変更する
具体的にはFemale → 0、Male → 1にしておく
from sklearn import preprocessing
from sklearn.preprocessing import OrdinalEncoder
# ラベルエンコーディング(OrdinalEncoder)
oe = preprocessing.OrdinalEncoder()
encoded = oe.fit_transform(df[['Gender']].values)
encodered_df = pd.DataFrame(encoded, columns= ['Gender_enc'])
# decoded = oe.inverse_transform(encoded) # エンコード前後を確認
df.insert(loc=2, column='Gender_enc', value=encodered_df) # 2カラム目にGender_encを挿入
df = df.drop(columns='Gender') # Gender列を削除
df.head()
「Gender」が「Gender_enc」になって、数値型になったのでOK
データ傾向の可視化
次に、データの傾向を確認してみよう
まずは雑にpairplot図を出してみる!
import seaborn as sns # データの傾向確認用
# pairplot図を出力
sns.pairplot(df)
この時に気にしたのは、
① 散布図から「disease」と相関がありそうなカラムの確認
→これだと相関が良くわからん(笑) → seabornのヒートマップで確認
➁ ヒストグラムより、データバイアス(偏り)の確認
→Gender(性別)で偏りがある(Maleが圧倒的に多い、後々消す対象かも?)
①が良くわからんかったので、seabornのヒートマップで確認
# データの可視化
import seaborn as sns
from matplotlib import pyplot as plt
sns.set(style='darkgrid')
plt.figure(figsize=(9,6))
sns.heatmap(df.corr(), annot=True)
おお!さっきより見やすい!
「Age」「Gender」「TP」が「disease」と相関が低そうだ…消す対象かな?
性別は男性より女性の方が肝疾患が罹りやすいと思っていたが(仕事上呑み会等で肝臓を酷使しているのかと)
そんなことはないということか…
また男性の母数の方が多いバイアスもあるため、特徴変数からは外した方が良さげ。
年齢も高いほど肝疾患リスクが大きいかなと思いきや、今回のデータではそこまで重要ではなかった。
ぐらいに思っておく!
また、今回のデータは特に欠損値などがなさそうなので、補間などは行わないでモデルに通してみる!
機械学習で予測
データセットの準備
ロードしたtrain.csvを特徴量に使う変数を”X”に、目的変数を"y"に分ける!
X = df.iloc[:, :10] # Age(0列目)〜AG_ration(10列目)を特徴量として使う
y = df.iloc[:,10:11] # disease(11列目)を目的変数として使う
# X.head() # 特徴変数の確認
# y.head() # 目的変数の確認
LightGBM
手始めにデータをそのままLightGBMに流してみる
# LightGBMで学習
from sklearn.metrics import accuracy_score, roc_auc_score
from sklearn.model_selection import KFold
import lightgbm as lgb
import numpy as np
# 警告を非表示
import warnings
warnings.simplefilter('ignore')
# パラメータ
FOLD = 5 # 交差検証の分ける回数
NUM_ROUND = 30000 # 学習ステップ数
VERBOSE_EVAL = 10000 # 学習結果の表示ステップ数
SEED = 42 # ランダム値のシード(再現性を持たせるため)
categorical_list = ['Gender_enc'] # カテゴリ変数
# LightGBMパラメータチューニング(よく使われる値で検証)
params = {
'objective' : 'binary',
'boosting_type' : 'gbdt', # default = gbdt
'num_leaves' : 63, # default = 31
'max_depth' : -1, # default = -1(上限なし)
'learning_rate' : 1e-2, # default = 0.1
'feature_fraction': 0.8, # default = 1.0
'bagging_freq' : 1, # default = 0
'random_state' : SEED, # default = None
'metric' : 'binary_logloss' # default = 'binary_logloss',
}
valid_auc = []
valid_acc = []
models = []
result_data = {}
# kFold交差検定で決定係数を算出し、各セットの平均値を返す
kf = KFold(n_splits=FOLD, shuffle=True, random_state=42)
for fold, (train_indices, valid_indices) in enumerate(kf.split(X)):
# 指定したindexで学習・評価データを分ける
X_train, X_valid = X.iloc[train_indices], X.iloc[valid_indices]
y_train, y_valid = y.iloc[train_indices], y.iloc[valid_indices]
train_data = lgb.Dataset(X_train, y_train)
valid_data = lgb.Dataset(X_valid, y_valid)
model = lgb.train(
params = params,
train_set = train_data,
valid_sets = [train_data, valid_data],
categorical_feature = categorical_list, # カテゴリ値のカラムを指定(やらんでも動く)
num_boost_round = NUM_ROUND,
early_stopping_rounds = 5,
verbose_eval = VERBOSE_EVAL,
evals_result = result_data
)
# 学習したモデルでバリデーションデータを予測
y_valid_pred = model.predict(X_valid)
# aucを計算(本問題の運営側 評価方法)
auc = roc_auc_score(y_valid.to_numpy().squeeze(), y_valid_pred) # 引数:正解データ & 予測データ
valid_auc.append(auc)
# 正解率を計算
acc = accuracy_score(y_valid.to_numpy().squeeze(),np.round(y_valid_pred)) # 引数:正解データ & 予測データ(四捨五入(銀行丸めになっている点は注意))
valid_acc.append(acc)
## 交差検証の正解率の平均
cv_acc = np.mean(valid_acc)
cv_auc = np.mean(valid_auc)
print('Accuracy: {}, auc: {}'.format(cv_acc, cv_auc))
精度は単純な0 or 1の比較では86%が正解、
aucとしては0.94であった。
他にもXGBoostやTabnetのトイモデルを実装し、バリデーション結果や、テスト予測結果を試しに提出したが、LightGBMの結果が最も良さそう!
この時使ったXGBoostやTabnetは以下で参照して欲しい
XGboostコード:xgboost.ipynb
Tabnetコード:tabnet.ipynb
特徴変数のブラッシュアップ
データ傾向を確認した時、diseaseとの相関性が低いことから、AgeやGender、TPを削除した方が良いかも知れないと仮説立てた
そこでそれらカラムを削除したデータを入力としてLightGBM(念のためTabnetなどにも同データを入力して)再学習してみる!
カラムの削除は以下のように行う。
(Ageだけ消す、Genderだけ消す、またはその両方を試してもみるといいかも)
# 相関の低いカラムを削除
X = X.drop(columns='Age')
X = X.drop(columns='Gender_enc')
X = X.drop(columns='TP')
X.head()
再度、LightGBMにデータを流して学習させた結果が以下である。
うーん、そこまで結果が変わらない…
Tabnetなどでも試したが、結果が悪化したパターンもあった
本問題と私の手法ではデータに何も施さなくてもよさそう…
Optunaによるハイパーパラメータ調整
ここまでで、モデルはLightGBM、データには何もしないのが良さそうだということが分かった!
最後にハイパーパラメータという、手動でノウハウ的に調節しなければならないパラメータがある…
ここまでに試したモデルのハイパーパラメータは、様々なコンペに参加した有識者が使ったモデルの平均や中央値、よく使われる値を使ったものである。
ただし本問題においても、その値がフィットするとは限らないので、さらに良いハイパーパラメータを探索したいが、
手動でやると面倒である(しかも楽しくないし、本質ではない…)
そこでハイパーパラメータ探索を自動化してくれるOptunaというライブラリを使ってみる!
# ライブラリのインストール
!pip install optuna
実際のコードは以下の通りである!
調整したいパラメータをリストや範囲で指定し、自分が指定した評価値が最大化するようなハイパーパラメータを探してもらう。
本問題ではAUCを最大化すべきだが、なぜかAccuracyの最大化をさせている・・・(ここ自分でやっといて謎)
# Optunaによるハイパラ探索
from sklearn.metrics import accuracy_score, roc_auc_score
from sklearn.model_selection import KFold
import lightgbm as lgb
import numpy as np
import optuna
# 警告を非表示
import warnings
warnings.simplefilter('ignore')
# パラメータ
FOLD = 5 # 交差検証の分ける回数
NUM_ROUND = 30000 # 学習ステップ数
VERBOSE_EVAL = 5000 # 学習結果の表示ステップ数
SEED = 42 # ランダム値のシード(再現性を持たせるため)
# categorical_list = ['Gender_enc'] # カテゴリ変数
def objective(trial):
# LightGBMパラメータチューニング(Optunaで探索)
params = {
'objective' : 'binary',
'boosting_type' : trial.suggest_categorical('boosting_type', ['gbdt', 'dart', 'goss']), # default = gbdt
'num_leaves' : trial.suggest_int('num_leaves', 10, 1000), # default = 31
'max_depth' : -1, # default = -1(上限なし)
'learning_rate' : trial.suggest_loguniform('learning_rate', 1e-8, 1.0), # default = 0.1
'feature_fraction': 0.8, # default = 1.0
'bagging_freq' : 1, # default = 0
'random_state' : SEED, # default = None
'metric' : trial.suggest_categorical('metrics', ['binary_logloss', 'rmse', 'auc']), # default = 'binary_logloss',
}
valid_auc = []
valid_acc = []
models = []
result_data = {}
# kFold交差検定で決定係数を算出し、各セットの平均値を返す
kf = KFold(n_splits=FOLD, shuffle=True, random_state=42)
for fold, (train_indices, valid_indices) in enumerate(kf.split(X)):
# 指定したindexで学習・評価データを分ける
X_train, X_valid = X.iloc[train_indices], X.iloc[valid_indices]
y_train, y_valid = y.iloc[train_indices], y.iloc[valid_indices]
train_data = lgb.Dataset(X_train, y_train)
valid_data = lgb.Dataset(X_valid, y_valid)
model = lgb.train(
params = params,
train_set = train_data,
valid_sets = [train_data, valid_data],
# categorical_feature = categorical_list, # カテゴリ値のカラムを指定(やらんでも動く)
num_boost_round = NUM_ROUND,
early_stopping_rounds = 5,
verbose_eval = VERBOSE_EVAL,
evals_result = result_data
)
# 学習したモデルでバリデーションデータを予測
y_valid_pred = model.predict(X_valid)
# aucを計算(本問題の運営側 評価方法)
auc = roc_auc_score(y_valid.to_numpy().squeeze(), y_valid_pred) # 引数:正解データ & 予測データ
valid_auc.append(auc)
# 正解率を計算
acc = accuracy_score(y_valid.to_numpy().squeeze(),np.round(y_valid_pred)) # 引数:正解データ & 予測データ(四捨五入(銀行丸めになっている点は注意))
valid_acc.append(acc)
# 交差検証の正解率の平均 accを最大化
cv_acc = np.mean(valid_acc)
cv_auc = np.mean(valid_auc)
print('Accuracy: {}, auc: {}'.format(cv_acc, cv_auc))
return cv_acc
# Optunaでハイパーパラメータ探索
study = optuna.create_study(direction='maximize') # 今回は正解率(Accuracy)を最大化(本当はAUC最大化の方が良い)
study.optimize(objective, n_trials=10) # 試行回数10回
[I 2022-10-11 07:44:57,989] A new study created in memory with name: no-name-25535e40-950e-4184-9784-6ad673f1c337
[5000] training's auc: 1 valid_1's auc: 0.968021
[10000] training's auc: 1 valid_1's auc: 0.967044
[15000] training's auc: 1 valid_1's auc: 0.965647
・・・
Training until validation scores don't improve for 5 rounds.
[I 2022-10-11 08:26:31,692] Trial 9 finished with value: 0.8564705882352941 and parameters: {'boosting_type': 'goss', 'num_leaves': 503, 'learning_rate': 0.10121026114047987, 'metrics': 'rmse'}. Best is trial 7 with value: 0.8658823529411764.
Early stopping, best iteration is:
[34] training's rmse: 0.239669 valid_1's rmse: 0.307551
Training until validation scores don't improve for 5 rounds.
Early stopping, best iteration is:
[36] training's rmse: 0.234832 valid_1's rmse: 0.323708
Accuracy: 0.8564705882352941, auc: 0.9411285861593524
n_trialsで指定した試行回数の中で、最も結果の良いハイパーパラメータを表示
print(study.best_params)
print(study.best_value)
{'boosting_type': 'gbdt', 'num_leaves': 713, 'learning_rate': 0.003700706181685094, 'metrics': 'binary_logloss'}
0.868235294117647
最適なハイパーパラメータで、再学習する!
# Optunaで探索したベストハイパラでLightGBMを再学習
params = {
'objective' : 'binary',
'boosting_type' : study.best_params['boosting_type'], # Optunaで探索した値を指定
'num_leaves' : study.best_params['num_leaves'], # Optunaで探索した値を指定
'max_depth' : -1, # default = -1(上限なし)
'learning_rate ': study.best_params['learning_rate'], # Optunaで探索した値を指定
'feature_fraction': 0.8, # default = 1.0
'bagging_freq' : 1, # default = 0
'random_state' : 0, # default = None
'metric' : study.best_params['metrics'], # Optunaで探索した値を指定
'seed' : SEED
}
valid_scores = []
valid_acc = []
models = []
result_data = {}
# kFold交差検定で決定係数を算出し、各セットの平均値を返す
kf = KFold(n_splits=FOLD, shuffle=True, random_state=42)
for fold, (train_indices, valid_indices) in enumerate(kf.split(X)):
X_train, X_valid = X.iloc[train_indices], X.iloc[valid_indices] # 指定したindexで学習・評価データを分ける
y_train, y_valid = y.iloc[train_indices], y.iloc[valid_indices]
train_data = lgb.Dataset(X_train, y_train)
valid_data = lgb.Dataset(X_valid, y_valid)
model = lgb.train(
params = params,
train_set = train_data,
valid_sets = [train_data, valid_data],
categorical_feature = categorical_list, # categorical_featureを設定
num_boost_round = NUM_ROUND,
early_stopping_rounds = 5,
verbose_eval = VERBOSE_EVAL,
evals_result = result_data
)
# 学習したモデルでバリデーションデータを予測
y_valid_pred = model.predict(X_valid)
# aucを計算(本問題の運営側 評価方法)
auc = roc_auc_score(y_valid.to_numpy().squeeze(), y_valid_pred) # 引数:正解データ & 予測データ
valid_auc.append(auc)
# 正解率を計算
acc = accuracy_score(y_valid.to_numpy().squeeze(),np.round(y_valid_pred)) # 引数:正解データ & 予測データ(四捨五入(銀行丸めになっている点は注意))
valid_acc.append(acc)
print('fold {} Accuracy:{}, auc:{}'.format(fold, acc, auc))
# モデルを保存
models.append(model)
# 交差検証の正解率の平均
cv_acc = np.mean(valid_acc)
cv_auc = np.mean(valid_auc)
print('Accuracy: {}, auc: {}'.format(cv_acc, cv_auc))
学習過程を表示してみる
# 最後のモデルだけ学習過程を表示
import matplotlib.pyplot as plt
plt.plot(result_data["training"]["binary_logloss"], color = "red", label = "train")
plt.plot(result_data["valid_1"]["binary_logloss"], color = "blue", label = "valid")
plt.legend()
plt.show()
特徴量の重要度を表示してみる
import matplotlib
# 特徴量の重要度を表示
lgb.plot_importance(model) # 最後のモデルデータのみ表示
今回のデータでは"AST_GOT"が最も重要そう…
最後に、この予測モデルをtestデータにも適用し、提出データを作成する!
# 評価用 test_csvデータ確認
testdata_df = pd.read_table('./dataset/test.csv', sep=',')
testdata_df.columns = ['Age', 'Gender', 'T_Bil', 'D_Bil', 'ALP', 'ALT_GPT', 'AST_GOT', 'TP', 'Alb', 'AG_ratio']
# ラベルエンコーディング(OrdinalEncoder)
# oe = preprocessing.OrdinalEncoder()
testdata_encoded = oe.fit_transform(testdata_df[['Gender']].values)
# データフレームを作成
encodered_testdata_df = pd.DataFrame(testdata_encoded, columns= ['Gender_enc'])
# decoded = oe.inverse_transform(encoded)
# エンコードしたデータに差し替え
testdata_df.insert(loc = 2, column= 'Gender_enc', value= encodered_testdata_df)
testdata_df = testdata_df.drop(columns='Gender') # エンコード前のGender列を削除
# 相関の低いカラムを削除 (Age, Gender_enc, TP, Alb)
# testdata_df = testdata_df.drop(columns='Age')
# testdata_df = testdata_df.drop(columns='Gender_enc')
# Kfoldで学習したモデルすべてで予測
test_y_preds = []
for model in models:
test_y_pred = model.predict(testdata_df)
test_y_preds.append(test_y_pred)
test_prediction = np.mean(test_y_preds, axis=0)
# CSV化
testdata_pred_df = pd.DataFrame(test_prediction)
testdata_pred_df.to_csv('./submit.csv', header=False)
結果の提出
最後にSIGNATEの投稿から予測結果を出力したcsvファイルを提出する!
少し待つと評価結果が出てくる!
これを繰り返して納得いくまで実施しよう(ただし1日5回までの制限があるので注意)
まとめ
・今回は「健診データから肝疾患の予測」コンペにLightGBMで参加し、AUC 0.92以上の結果を残した。
・LightGBMを初めて使ったが、お手軽に予測モデルを作成することができ、なおかつ強かった
・Kfold検証による比較的精度の高い検証方法を学習、またOptunaの使い方を覚えることでハイパラ探索もある程度自働化できるようになった。
TODO (to me)
TabNetやXGBoostも試したので、本記事にいずれ追記し、評価結果も比較する。