0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kaggle的】LightGBM とOptunaで住宅価格予測【RMSE: 0.440】

Posted at

はじめに

前々回、california_housingデータセットを使って、回帰を行った。今回はOptunaを導入して、ハイパーパラメータの最適化を行う。

できたこと

  • データセット: california_housing
  • 問題: 回帰
  • 機械学習モデル: LightGBM
  • 前処理, 特徴量エンジニアリング: 何もせず
  • 交差検証: K-Fold (5-Fold)
  • ハイパーパラメータチューニング
    • ライブラリ: Optuna
    • 項目: num_leaves, learning_rate, feature_fraction, etc...
    • 試行回数: 50
  • 評価指標: RMSE
  • CVスコア: 0.4494 (前々回は、0.4739)
  • テストスコア: 0.4402 (前々回は、0.4622)
=== RMSE for test data ===
0.44027

前々回(チューニングなし)に比べて、わずかにスコアアップした。よかった。

できてないこと

EDAは割愛する。

実装

ライブラリ

# 関連ライブラリをインポート
import lightgbm as lgb
import numpy as np
import optuna
import pandas as pd
import random

# scikit-learn関連をインポート
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split, KFold, StratifiedKFold
from sklearn.metrics import mean_squared_error

# warningを非表示
import warnings
warnings.filterwarnings('ignore')

出力を一定にするため、乱数を固定する。

SEED = 42

# 参考: 乱数固定
def seed_everything(seed=42):
    random.seed(seed)
    np.random.seed(seed)
    #os.environ['PYTHONHASHSEED'] = str(seed)
    # torch.manual_seed(seed)
    # torch.cuda.manual_seed(seed)
    # torch.backends.cudnn.daterministic = True
seed_everything(SEED)

データ読み込み

data = fetch_california_housing()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = pd.Series(data.target)

print(X.shape)
print(y.shape)
(20640, 8)
(20640,)

データを訓練用(検証用含む)と、テスト用に分割する。インデックス番号を振りなおしているのは、K-Foldを効率的に行うため。

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=SEED)

X_train.reset_index(drop=True, inplace=True)
y_train.reset_index(drop=True, inplace=True)
X_test.reset_index(drop=True, inplace=True)
y_test.reset_index(drop=True, inplace=True)

print(X_train.shape) 
display(X_train.head(3))
print(y_train.shape) 
display(y_train.head(3))
print(X_test.shape) 
display(X_test.head(3))
print(y_test.shape) 
display(y_test.head(3))

訓練データが1.6万件、テストデータが0.4万件になった。(データフレームの表示は割愛)

(16512, 8)
(16512,)
(4128, 8)
(4128,)

交差検証の準備

今回は5-Foldとする。

kf = KFold(n_splits=5, shuffle=True, random_state=SEED)

ハイパーパラメータの最適化

Optunaを使って、ハイパーパラメータを最適化する。最適化する項目と探索範囲はChat-GPTに聞いた…笑。

categorical_features = [] # カテゴリ変数なし
def objective(trial):
    params = {
        'objective': 'regression',
        'metric': 'rmse',
        'boosting_type': 'gbdt',
        'verbosity': -1,
        'num_leaves': trial.suggest_int('num_leaves', 20, 150),
        'learning_rate': trial.suggest_float('learning_rate', 1e-3, 1e-1, log=True),
        'feature_fraction': trial.suggest_float('feature_fraction', 0.4, 1.0),
        'bagging_fraction': trial.suggest_float('bagging_fraction', 0.4, 1.0),
        'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
        'seed': SEED 
    }

    rmse_scores = []

    # 交差検証を実行
    for tr_index, val_index in kf.split(X_train):
        # 訓練データと検証データに分割
        X_tr = X_train.loc[tr_index]
        X_val = X_train.loc[val_index]
        y_tr = y_train.loc[tr_index]
        y_val = y_train.loc[val_index]

        lgb_tr = lgb.Dataset(X_tr, y_tr, categorical_feature=categorical_features)
        lgb_val = lgb.Dataset(X_val, y_val, reference=lgb_tr, categorical_feature=categorical_features)

        # 訓練
        model = lgb.train(params, 
                          lgb_tr, 
                          valid_sets=[lgb_val], 
                          #num_boost_round=10000,
                          callbacks=[
                              lgb.early_stopping(
                                  stopping_rounds=100,
                                  verbose=False),
                            lgb.log_evaluation(period=100)
                          ]
                          )

        # 予測
        y_pred_val = model.predict(X_val, num_iteration=model.best_iteration)
        rmse = np.sqrt(mean_squared_error(y_val, y_pred_val))
        rmse_scores.append(rmse)
    
    # 交差検証の平均スコアを返す
    return np.mean(rmse_scores)

最適化を実行する。

# 最適化を実行
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=50)

最適化したパラメータを表示する。

print('Best parameters:', study.best_params)
Best parameters: {'num_leaves': 90, 'learning_rate': 0.07995616486006746, 'feature_fraction': 0.6460671201267157, 'bagging_fraction': 0.7290593706997489, 'bagging_freq': 3, 'min_child_samples': 25}

最適化したハイパーパラメータで再訓練

最適化したハイパーパラメータで再度訓練する。まずはパラメータを設定する。

params_tuned = {
    'objective': 'regression',
    'metric': 'rmse',
    'boosting_type': 'gbdt',
    'verbosity': -1,
    'num_leaves': study.best_params['num_leaves'], #trial.suggest_int('num_leaves', 20, 150),
    'learning_rate': study.best_params['learning_rate'], #trial.suggest_float('learning_rate', 1e-3, 1e-1, log=True),
    'feature_fraction': study.best_params['feature_fraction'], #trial.suggest_float('feature_fraction', 0.4, 1.0),
    'bagging_fraction': study.best_params['bagging_fraction'], #trial.suggest_float('bagging_fraction', 0.4, 1.0),
    'bagging_freq': study.best_params['bagging_freq'], #trial.suggest_int('bagging_freq', 1, 7),
    'min_child_samples': study.best_params['min_child_samples'], #trial.suggest_int('min_child_samples', 5, 100),
    'seed': SEED 
}

次に、訓練結果を格納するための変数を用意する。

# 結果格納用変数
y_pred_oof = np.zeros(X_train.shape[0])
y_preds_test = []
rmse_scores = []
models = []

訓練実施。

# 再訓練
for tr_index, val_index in kf.split(X_train, y_train):
    # 訓練データと検証データに分割
    X_tr = X_train.loc[tr_index]
    X_val = X_train.loc[val_index]
    y_tr = y_train.loc[tr_index]
    y_val = y_train.loc[val_index]

    lgb_tr = lgb.Dataset(X_tr, y_tr, categorical_feature=categorical_features)
    lgb_val = lgb.Dataset(X_val, y_val, reference=lgb_tr, categorical_feature=categorical_features)

    # 訓練
    model = lgb.train(params_tuned, 
                      lgb_tr, 
                      valid_sets=[lgb_val], 
                      #num_boost_round=10000,
                      callbacks=[
                        lgb.early_stopping(
                            stopping_rounds=100,
                            verbose=False),
                        lgb.log_evaluation(period=100)
                        ]
                      )

    # 予測
    y_pred_val = model.predict(X_val, num_iteration=model.best_iteration)
    y_pred_test = model.predict(X_test, num_iteration=model.best_iteration)

    # rmseを計算
    rmse = np.sqrt(mean_squared_error(y_val, y_pred_val))

    # 予測値やスコア、学習済みモデルを格納
    y_pred_oof[val_index] = y_pred_val
    y_preds_test.append(y_pred_test)
    rmse_scores.append(rmse)
    models.append(model)

評価

訓練結果を確認する。まずは各foldでの検証データ(validation)に対するRMSEを表示する。0,435から0.468くらい。前々回のOptuna無しの時は、0.468から0.478くらいだったので、良化したっぽい。

# rmseを表示
for i in range(len(rmse_scores)):
    print(f'fold{i+1} rmse: {rmse_scores[i]:.4f}')
fold1 rmse: 0.4681
fold2 rmse: 0.4448
fold3 rmse: 0.4398
fold4 rmse: 0.4590
fold5 rmse: 0.4352

平均を取り、CVスコアを算出すると、0.4494となった。前々回は0.4739だったので、平均値も良化。

cv_score = np.mean(rmse_scores)
print('=== CV score (rmse) ===')
print(f'{cv_score:.4f}')
=== CV score (rmse) ===
0.4494

ハイパーパラメータチューニングの効果が見られたか…。

テスト

一般的なKaggleコンペなら、テストデータに対する予測を行い、形式を整理し、Submitする。今回はScikit-learn上でデータセットをダウンロードし、最初に訓練データとテストデータに分割したので、テストデータの正解ラベルも手元にあり、RMSEスコアを算出可能。まずは各foldでのテストデータに対する予測値を表示する。

# 各foldのテストデータに対する予測値を表示
for i in range(len(y_preds_test)):
    print(f'fold{i+1}: {y_preds_test[i][:5]} ...')
fold1: [0.48554674 1.24362198 5.08410152 2.3051161  2.3623417 ] ...
fold2: [0.52486522 0.91217026 5.1110846  2.39300856 2.4203427 ] ...
fold3: [0.47965315 0.83645024 4.94657118 2.32503299 2.34725133] ...
fold4: [0.50302413 0.94841206 5.1756785  2.45468694 2.33222264] ...
fold5: [0.4919892  0.8870616  5.06584356 2.35460799 2.26655994] ...

最初の5個しか表示していないが、変な値は出ていなさそう。平均を取って、最終的な予測値とする。この辺りはいろいろやり方があると思うが、書籍等を参考にして、このやり方を採用する。軽くアンサンブル学習になっているがよいところか?

y_sub = sum(y_preds_test) / len(y_preds_test)
print(f'{y_sub[:5]} ...')
[0.49701569 0.96554323 5.07665587 2.36649051 2.34574366] ...

目視で見た感じ、計算間違いはしていなさそう。今回、提出はしないが、csvファイルを作成する。

# 提出用ファイルを作成
sub = pd.DataFrame({'target': y_sub})
sub.to_csv('submission.csv', index=False)
display(sub.head())

今回、テストデータの正解ラベルは手元にあるので、採点する。

# テストデータの予測値を使って採点
rmse_sub = np.sqrt(mean_squared_error(y_test, y_sub))
print('=== RMSE for test data ===')
print(f'{rmse_sub:.5f}')
=== RMSE for test data ===
0.44027

結果は、0.4402。CVスコアが0.4494なので、過学習はしていなさそう。むしろアンサンブルの効果が出てる?前々回(Optuna無し)と比べて、0.4622から0.4402とスコアアップしており、ハイパーパラメータチューニングの効果が見て取れた。よかった。

おわりに

california_housingデータセットを使って回帰問題を解いた。Optunaを導入したところ、スコアアップした。効果はわずかだが、実際のコンペでもこれくらいのイメージ。上位を狙うなら、チューニングは欠かせないか…。特徴量エンジニアリングと違って、基本的にやれば効果につながるのがハイパーパラメータチューニングのいいところか(過学習は別の話…)。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?