Pythonデータ分析(勾配ブースティング①分類)
nopoです。コロナ後遺症患者です。
すっかり長期療養組になってしまいましたが、カメの歩みの如くでも徐々に体調は良くなっています。
日中の活動に必要な臥位休息時間が8~10時間だったものが最近は3時間程になりました。PC作業時間が長くとれるので勉強が捗ります。
残念ながら現職復帰レベルまでは改善せずそろそろ職を失いそうですが、それはそれとして黙々と機械学習の理解を深めていこうと思います。
今回はSIGNATEの練習問題:債務不履行リスクの低減をやってみました。
分類問題で、相棒をBardからBingに変更してLightGBMを使用しました。
Bingの方がコードのミスが少なく、しかもコピペしたコードが間違っていることを指摘すると「申し訳ありません。私の誤りです。RandomUnderSamplerクラスのfit_resampleメソッドを使用してください。」と謝ってくれました。
Googleが好きなので応援したいのですが、しばらくBingをお供にさせていただきます。
データ前処理
特徴量はidを除くと8です。
・loan_amnt 借入総額
・term 返済期間
・interest_rate 金利
・grade グレード
・employment_length 勤続年数
・purpose 借入の目的
・credit_score 信用スコア
・application_type 借入時の申請方式
・loan_status 返済状況(目的変数)
loans = pd.read_csv('train.csv')
# loan_statusカラムの値がFullyPaid、またはChargedOffであるデータを取り出し、変数loansに再代入
loans = loans[(loans['loan_status'] == 'FullyPaid') | (loans['loan_status'] == 'ChargedOff')]
loans = loans.drop(columns=['id'])
# 欠損値を含む行を削除
loans.isna().sum()
loans = loans.dropna()
可視化
●interest_rateとcredit_scoreを箱ひげ図でプロット
# 箱ひげ図を作成
sns.boxplot(x='loan_status'y='interest_rate',data = loans)
plt.title('Loan Status vs interest_rate')
plt.xlabel('Loan Status')
plt.ylabel('interest_rate')
plt.show()
●term、grade 、purpose 、application_type を積み上げ棒グラフでプロット
# 説明変数をインデックス(行)、目的変数をカラム(列)としてクロス集計
cross_term = pd.crosstab(loans['term'], loans['loan_status'], margins=True)
# ChargedOffカラムをAllカラムで割り、変数c_rateに代入
cross_term['c_rate'] = cross_term['ChargedOff'] / cross_term['All']
# FullyPaidカラムをAllカラムで割り、変数f_rateに代入
cross_term['f_rate'] = cross_term['FullyPaid'] / cross_term['All']
#積み上げ棒グラフ
cross_term = cross_term.drop(index = ['All'])
df_bar = cross_term[['c_rate', 'f_rate']]
df_bar.plot.bar(stacked=True)
plt.title('返済期間ごとの貸し倒れ率と完済率')
plt.xlabel('期間')
plt.ylabel('割合')
plt.show()
借金の総額や勤続年数は関連が浅かったので除外しました。借り入れ目的については少し悩みましたが採用してみました。
標準化・ダミー変数化
分析しやすいように、数値データを標準化(平均値0で標準偏差に変換)しました。また、質的データはダミー変数化しています。
# 標準化
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
cols_to_normalize = ['interest_rate','credit_score']
loans[cols_to_normalize] = scaler.fit_transform(loans[cols_to_normalize])
# ダミー変数化
X = loans[['term','interest_rate','grade','purpose', 'credit_score', 'application_type']]
target = loans['loan_status']
X_dum = pd.get_dummies(X)
target_dum = pd.get_dummies(target)
# 変数target_dumからFullyPaidカラムを削除し、変数target_dumに再代入
target_dum = target_dum.drop(columns='FullyPaid')
データ分割
目的変数はFullyPaid:184426、 ChargedOff:44545と偏りがある為、分割する際も値が固まらないようにstratifyを使用しています。
#データ分割
from sklearn.model_selection import train_test_split
X_trainval, X_test, y_trainval, y_test = train_test_split(X_dum, target_dum, random_state=20, stratify=target_dum['ChargedOff'])
# 学習データと検証データに75:25の割合で2分割する
X_train, X_val, y_train, y_val = train_test_split(X_trainval, y_trainval, test_size=0.25, shuffle=True,random_state=20,stratify=y_trainval)
ダウンサンプリング
さらに「0:FullPay」のデータ数を減らして「1:ChargedOff」のデータ数との差を縮めるダウンサンプリングを使用。
(「1:ChargedOff」のデータをランダムに増やすオーバーサンプリングでは精度が低かったので今回は不採用)
from imblearn.under_sampling import RandomUnderSampler
# 正例の数を保存
positive_count_train = int(y_train.sum())
print('positive count: {}'.format(positive_count_train))
# 負例をダウンサンプリング
rus = RandomUnderSampler(sampling_strategy={0:positive_count_train*2, 1:positive_count_train}, random_state=40)
# 学習用データに反映
y_train = y_train.iloc[:, 0]
x_train_resampled, y_train_resampled = rus.fit_resample(X_train, y_train)
print('y_train_undersample:\\n {}'.format(pd.Series(y_train_resampled).value_counts()))
LightGBM
#LightGBM
import optuna
from lightgbm import LGBMClassifier
def objective(trial):
# モデルの作成
model = LGBMClassifier(boosting_type='gbdt', objective='binary',
metric='binary_error', verbosity=-1)
# モデルの学習
model.fit(x_train_resampled, y_train_resampled)
# 検証データに対するスコアの計算
score = model.score(X_val, y_val)
# スコアを返す
return score
# Optunaによるハイパーパラメータの最適化
sampler = optuna.samplers.TPESampler(seed=0)
study = optuna.create_study(direction='maximize', sampler=sampler)
study.optimize(objective, n_trials=50)
LightGBMは、勾配ブースティングアルゴリズムに基づく分類器です。回帰にも分類にも使えて便利。今回は二値分類のパラメータに設定しています。
Optunaは、ハイパーパラメータの探索と最適化をベイズ統計を使用して自動化してくれるそうです。よくわかってないので、ドキュメント等読んで勉強せねばです。
モデルを回すたびに精度が変わってしまうので、TPESampler(seed=0)で固定しています。(これに気づかず何回調整しても精度が安定しないなと思っていました・・・)
【Bing回答】
TPESampler(seed=0) は、Optunaの乱数シードを固定するためのものです。Optunaでは最適化の過程で乱数を利用しており、回ごとに乱数がランダムに変わるので、最適化の結果も変わってしまいます。生成される乱数の値を回によらず同じとするためには、乱数シードを固定する必要があります。seed引数に、乱数シード(42がよく使われる)を指定すれば乱数シードが固定され、繰り返しても同じ結果となることが保証されます。
f1_score
from sklearn.metrics import f1_score
# 最適なハイパーパラメータの取得
best_params = study.best_params
# モデルの作成
model2 = LGBMClassifier(**best_params)
# モデルの学習
model2.fit(X_trainval, y_trainval)
# 予測値の計算
y_train_pred = model2.predict(X_trainval)
y_test_pred = model2.predict(X_test)
# f1スコアの計算
f1_train = f1_score(y_trainval, y_train_pred)
f1_test = f1_score(y_test, y_test_pred)
print('f1 score (train):', f1_train)
print('f1 score (test):', f1_test)
f1 score (train): 0.0845046900659671
f1 score (test): 0.07437051859262965
上記で取得した最適なパラメータでモデルを学習させます。
評価値は「F1Score」を使用します。0~1の値をとり、精度が高いほど大きな値となります。この時点ではとてもスコアが低いです。
閾値の調整
LightGBMには閾値があり、閾値を超えるか超えないかで分類されます。デフォルトは0.5なので、y_test_probaの値を見ながら良い感じに分類できる値へ調整します。
# 陽性確率の計算
y_test_proba = model2.predict_proba(X_test)[:, 1]
# 閾値の設定
threshold = 0.1853
# 予測値の計算
y_test_pred = (y_test_proba >= threshold).astype(int)
# f1スコアの計算
f1_test = f1_score(y_test, y_test_pred)
print('f1 score (test):', f1_test)
f1 score (test): 0.4125193998965339
一気に精度が上がりました!
提出データ作成
提出データも学習データと同じ形式にする必要があります。
今回のモデルでは学習時とほぼ同じ精度を出すことができ、上位30%に入ることができました。
#提出データ作成
test = pd.read_csv('test.csv')
test2 = test[['term','interest_rate','grade','purpose', 'credit_score', 'application_type']]
#標準化
cols_to_normalize = ['interest_rate','credit_score']
test2[cols_to_normalize] = scaler.fit_transform(test2[cols_to_normalize])
#ダミー変数化
test2 = pd.get_dummies(test2)
# テストデータで予測
test_proba = model2.predict_proba(test2)[:, 1]
# 予測値の計算
test['y'] = (test_proba >= threshold).astype(int)
test[['id', 'y']].to_csv('./submit.csv', header=False, index=False)
過学習について
LightGBM+Optunaで匙加減も分からずL1正規化やL2正規化をハイパーパラメータ調整に突っ込んでいると、学習データはaccuracy0.92をマークするもテストデータは0.79と典型的な過学習に陥ってしまいました。
(結局ハイパーパラメータの部分に自分で手を出さず自動にしたらテストデータも精度が上がりました…)
Bingに聞いたら特徴量エンジニアリングやアンサンブル学習等の解決案は出してくれるものの、そもそも過学習を起こす背景が私には理解できず悩みました。これについてはQuitaのこの記事に助けられました。↓
執筆されたのは2017年(最終更新2019年)ですが、大変参考になりました。私の場合は学習曲線の概念がなく、特徴量の理解が甘かったのでその辺りをやり直しました。
やっぱりきちんと理解するためには体系的な学習は必要だと思います。生成系AIへのプロンプト次第かもしれませんが、分からない事が多過ぎて短絡的なものになってしまってる感が否めません。
ちなみにこんな感じのプロンプトが良いそうです。真似て自作してみるものの、一からこんな風にはなかなか思い付かないです。
療養に割いてきた時間が学習にシフトでき始めたので引き続き励みます。
目指すは収入そこそこの在宅ワーカー。
寛解しなくとも何とかして収入を得ねばならなくなった時、武器を少しでも多く持っておきたいものです。