Higu`s diary

新米データサイエンティストのブログ。技術についてゆるく書きます〜

Pythonにおける勾配ブースティング予測モデルをラクチンに作成するラッパーを公開しました

こんばんは、ひぐです。

今回はPythonの勾配ブースティングライブラリを使いやすくしたラッパーについて紹介します。 今回紹介するラッパーを使うと以下のメリットがあります。

  • PandasのDataFrameといくつかの引数を渡すだけで予測結果が返ってくる
  • 本来はそれぞれ使い方が微妙に異なるライブラリを、全く同じ記法で使える
  • 出力した予測値を正解データとすぐに比較できる、可視化メソッドが使える
  • パラメータやValidationの分け方を簡単に指定できる
  • ターゲットエンコーディングが必要な場合、列と関数を渡すことで自動でリークしないように計算してくれる

機械学習を用いたデータ分析で必ず必要になる予測モデルを作成するプロセスですが、
これらをいつも同じ使い方で使用できるのは大きなメリットだと思います!

よければ是非使ってください!

使い方

用意するもの
- train/testデータ(DataFrame)
- ハイパーパラメータ(dict)

まず使用するハイパーパラメータを定義します。

from script import RegressionPredictor
cat_params = {

    'loss_function': 'RMSE',
    'num_boost_round': 5000,
    'early_stopping_rounds': 100,
}

xgb_params = {
        'num_boost_round':5000,
        'early_stopping_rounds':100,
        'objective': 'reg:linear',
        'eval_metric': 'rmse',
    }

lgbm_params = {
    'num_iterations': 5000,
    'learning_rate': 0.05,
    'objective': 'regression',
    'metric': 'rmse',
    'early_stopping_rounds': 100}

そしてインスタンスの呼び出し、学習します。

catPredictor = RegressionPredictor(train_df, train_y, test_df, params=cat_params,n_splits=10, clf_type='cat')
catoof, catpreds, catFIs=catPredictor.fit()


xgbPredictor = RegressionPredictor(train_df, train_y, test_df, params=xgb_params,n_splits=10, clf_type='xgb')
xgboof, xgbpreds, xgbFIs = xgbPredictor.fit()


lgbPredictor = RegressionPredictor(train_df, train_y, test_df, params=lgbm_params,n_splits=10, clf_type='lgb')
lgboof, lgbpreds, lgbFIs = lgbPredictor.fit()

rfPredictor = RegressionPredictor(train_df, train_y, test_df, sk_model=RandomForestRegressor(rf_params), n_splits=10, clf_type='sklearn')
rfoof, rfpreds, rfFIs = rfPredictor.fit()

これだけです!
fitの返り値はそれぞれ、trainの予測データ、testの予測データ、Feature Importanceのnumpy arrayです 。 Kaggleなどのデータ分析の場合、これらをcsvにするだけですぐに提出できるようになります。

予測値についてデータ可視化したい場合は以下のようにします。

lgbPredictor.plot_FI(50)
lgbPredictor.plot_pred_dist()

ソースコード

class RegressionPredictor(object):
    '''
    回帰をKfoldで学習するクラス。
    TODO:分類、多クラス対応/Folderを外部から渡す/predictのプロット/できれば学習曲線のプロット
    '''
    def __init__(self, train_X, train_y, split_y, test_X, params=None, Folder=None, sk_model=None, n_splits=5, clf_type='xgb'):
        self.kf = Folder(n_splits=n_splits)
        self.columns = train_X.columns.values
        self.train_X = train_X
        self.train_y = train_y
        self.test_X = test_X
        self.params = params
        self.oof = np.zeros((len(self.train_X),))
        self.preds = np.zeros((len(self.test_X),))
        if clf_type == 'xgb':
            self.FIs = {}
        else:
            self.FIs = np.zeros(self.train_X.shape[1], dtype=np.float)
        self.sk_model = sk_model
        self.clf_type = clf_type

    @staticmethod
    def merge_dict_add_values(d1, d2):
        return dict(Counter(d1) + Counter(d2))
   
    def rmse(self):
        return int(np.sqrt(mean_squared_error(self.oof, self.train_y)))
    
    def get_model(self):
        return self.model

    def _get_xgb_callbacks(self):
        '''nround,early_stopをparam_dictから得るためのメソッド'''
        nround=1000
        early_stop_rounds=10
        if self.params['num_boost_round']:
            nround=self.params['num_boost_round']
            del self.params['num_boost_round']

        if self.params['early_stopping_rounds']:
            early_stop_rounds=self.params['early_stopping_rounds']
            del self.params['early_stopping_rounds']
        return nround ,early_stop_rounds

    def _get_cv_model(self, tr_X, val_X, tr_y, val_y, val_idx):

        if self.clf_type == 'cat':
            clf_train =Pool(tr_X,tr_y)
            clf_val =Pool(val_X,val_y)
            clf_test =Pool(self.test_X)
            self.model=CatBoost(params=self.params)
            self.model.fit(clf_train,eval_set=[clf_val])
            self.oof[val_idx]=self.model.predict(clf_val)
            self.preds += self.model.predict(clf_test) / self.kf.n_splits
            self.FIs += self.model.get_feature_importance()

        elif self.clf_type == 'lgb':
            clf_train = lgb.Dataset(tr_X, tr_y)
            clf_val = lgb.Dataset(val_X, val_y, reference=lgb.train)
            self.model = lgb.train(self.params, clf_train, valid_sets=clf_val)
            self.oof[val_idx] = self.model.predict(val_X, num_iteration=self.model.best_iteration)
            self.preds += self.model.predict(self.test_X, num_iteration=self.model.best_iteration) / self.kf.n_splits
            self.FIs += self.model.feature_importance()

        elif self.clf_type == 'xgb':
            clf_train = xgb.DMatrix(tr_X, label=tr_y, feature_names=self.columns)
            clf_val = xgb.DMatrix(val_X, label=val_y, feature_names=self.columns)
            clf_test = xgb.DMatrix(self.test_X, feature_names=self.columns)
            evals = [(clf_train, 'train'), (clf_val, 'eval')]
            evals_result = {}

            nround,early_stop_rounds=  self._get_xgb_callbacks()
            self.model = xgb.train(self.params,
                                    clf_train,
                                    num_boost_round=nround,
                                    early_stopping_rounds=early_stop_rounds,
                                    evals=evals,
                                    evals_result=evals_result)

            self.oof[val_idx] = self.model.predict(clf_val)
            self.preds += self.model.predict(clf_test) / self.kf.n_splits
            self.FIs = self.merge_dict_add_values(self.FIs, self.model.get_fscore())

        elif self.clf_type == 'sklearn':
            self.model = self.sk_model
            self.model.fit(tr_X, tr_y)
            self.oof[val_idx] = self.model.predict(val_X)
            self.preds += self.model.predict(self.test_X) / self.kf.n_splits
            self.FIs += self.model.feature_importances_
        else:
            raise ValueError('clf_type is wrong.')

    def fit(self):
        for train_idx, val_idx in self.kf.split(self.train_X, self.train_y):
            X_train = self.train_X.iloc[train_idx, :]
            X_val = self.train_X.iloc[val_idx, :]
            y_train = self.train_y[train_idx]
            y_val = self.train_y[val_idx]
            self._get_cv_model(X_train, X_val, y_train, y_val, val_idx)
        print('this self.model`s rmse:',self.rmse())

        return self.oof, self.preds, self.FIs

    def plot_FI(self, max_row=50):
        plt.figure(figsize=(10, 20))
        if self.clf_type == 'xgb':
            df = pd.DataFrame.from_dict(self.FIs, orient='index').reset_index()
            df.columns = ['col', 'FI']
        else:
            df = pd.DataFrame({'FI': self.FIs, 'col': self.columns})
        df = df.sort_values('FI', ascending=False).reset_index(drop=True).iloc[:max_row, :]
        sns.barplot(x='FI', y='col', data=df)
        plt.show()
    
    def plot_pred_dist(self):
        fig, axs = plt.subplots(1, 2, figsize=(18, 5))
        sns.distplot(self.oof, ax=axs[1], label='oof')
        sns.distplot(self.train_y, ax=axs[0], label='train_y')
        sns.distplot(self.preds, ax=axs[0], label='test_preds')
        plt.show()

以上です!
未実装な部分はいっぱいあるので逐次修正していきたいと思います!
ゆくゆくは親クラスを作って、分類回帰でクラスを分けて継承していくみたいにしたいと思います。
こういうふうに実装した方がいいよなど知見があればコメント頂けたら幸いです。

最後まで読んでいただきありがとうございました~

google-site-verification: google1c6f931fc8723fac.html