たかいとの備忘録

自然言語処理や機械学習のことについて学んだことを忘れないように書いていけたらと思います.

【FDUA】第一回 金融データ活用チャレンジの戦い方を考える

はじめに

今更ながら開会式を視聴させていただきました. コンペ開催の目的など,とてもポジティブなものであり,そのために多くの方の協力のもと一年ほど時間をかけているとのことで,コンペが盛り上がるといいなと思っております.(記事投稿の理由も少しでもコンペの活性化に貢献することを意識しており,最低限の実装とコンペを進める上で手詰まりを避けるためのアイデアをいくつか書かせていただきました.)

当該コンペはディスカッション(フォーラム)がなく,baselineコードなどの共有が少ないため,最初のサブまでに時間がかかってしまっているように感じております. 記事執筆現在,sample_submission以外を提出できている参加者は110名程度(参加者の10%ほど)かと思います. また,分析環境が準備されているコンペは少なく,不慣れな分析環境での実装は,自身も含め苦戦の原因となっているのが,slackを見て感じているところです.

slackの自己紹介チャンネルでも「初めて挑戦します!」という方が多かったので,コンペでの一番の楽しみである,特徴量作成,自身の工夫を加えるというところにぜひ時間を割いてもらい,少しでもデータ分析コンペの楽しさを実感してもらえたら幸いです.(特にコンペ期間も1ヶ月弱と短いため,何もできずに終わってしまったというのを回避してもらえたら幸いです!) なお,本記事はコンペで共有されているチュートリアルを元に作成しております.

まだ始まったばかりで,的外れな意見もあるかと思いますが,何かの参考になれば幸いです.

今回のコンペに取り組むにあたって

今回のコンペのデータを見て,一番最初に頭によぎったのはKaggleAmerican Express - Default Predictionというコンペです. 該当コンペに比べて,今回のコンペデータは非常にコンパクトであり,分析環境も提供されていることから,第一回に適したコンペだと感じました.

作成が必要な関数

ルールの「実装方法」にも書いてある通り,前処理,学習,予測の3つにコードを分け実装が必要となります.

①Preprocessing「前処理」

  提供データを読み込み、前処理を施し、モデルに入力が可能な状態に変換するモジュール。

  get_train_dataやget_test_dataのように、学習用と評価用を分けて、前処理を行う関数を定義。

 ②Learning「学習」

  ①の出力を読み込み、モデルを学習し、学習済みモデルを出力するモジュール。

  学習済みモデルや特徴量、クロスバリデーションの評価結果を出力する関数等を定義。

 ③Predicting「予測」

  ①で作成した評価要データ及び②で作成した学習済みモデルを読み込み、予測結果を出力するモジュール。

まずはこれらに沿って,baselineとなるような実装を共有できたらと思います.

①Preprocessing「前処理」

基本的にはチュートリアルにある通り,以下を実行します.

%run ../fdua-databricks-utils/utils $mode="set_schema"

その上で以下のような関数を準備すれば,問題ないかと思います.

import pandas as pd

def preprocessing(input_df:pd.DataFrame)->pd.DataFrame:
    output_df = input_df.copy()
    # いろいろ前処理を追加
    return output_df

def read_train_data()->pd.DataFrame:
    train = spark.table("main.db_fdua_org.train").toPandas()
    return preprocessing(train)

def read_simple_test_data()->pd.DataFrame:
    test = spark.table("main.db_fdua_org.test").toPandas()
    return preprocessing(test)

関数「preprocessing」に,いろいろな工夫を加えるのがコンペの醍醐味の一つかと思いますので,何も特徴量生成や欠損処理を行っておりません. また,今回のコンペは時系列データでもありますので,前月との差分などの特徴量も有効な特徴量になり得るかと思います. そのためには,評価データの読み込みを以下のように変更することで,学習データから評価データの過去分を連結した評価データを作成することができます.

def read_test_data()->pd.DataFrame:
    train = spark.table("main.db_fdua_org.train").toPandas()
    test = spark.table("main.db_fdua_org.test").toPandas()
    test_gids = test['gid'].unique()
    test = pd.concat([train[train['gid'].isin(test_gids)], test]).sort_values(['gid', 'yyyymm']).reset_index(drop=True)
    return preprocessing(test)

とりあえず今回は煩雑な説明を省くために「read_simple_test_data」を用いたbaselineの共有となります.

②Learning「学習」

baselineとして,現在使用率の非常に高いlightgbmという決定木ベースの手法の実装を共有します. 細かいことは,公式ドキュメントなどを確認してもらえたら幸いです. 初心者の方は,u++さんの「初手LightGBM」をする7つの理由をまずは読むのがオススメです. 自身としては,高速,高い精度が見込める,特徴量のスケーリングが不要(決定木系モデルなので)の三点が特にポイントかと思います.

なお,この学習に関する実装は,自身も普段使っている環境ではなく,mlflowも普段使っていないため書き方に自信がありません. より良い書き方などアドバイスあればコメントいただけると幸いです.

import numpy as np
import os
from tqdm import tqdm
import gc
import random

import mlflow
import mlflow.pyfunc
import lightgbm as lgb
from sklearn.model_selection import StratifiedKFold, GroupKFold
from sklearn.metrics import roc_auc_score

class CFG:
    VER = 1
    AUTHOR = 'takaito'
    METHOD = 'lightgbm'
    COMPETITION = 'FDUA'
    target_col = 'target_flag'
    seed = 42
    num_fold = 5
    boosting_type = 'gbdt'
    lgb_params = {
        'objective': 'binary',
        'metric': 'auc',
        'boosting': boosting_type,
        'seed': seed,
    }

def get_groupkfold(train, target_col, group_col, n_splits):
    kf = GroupKFold(n_splits=n_splits)
    generator = kf.split(train, train[target_col], train[group_col])
    fold_series = []
    for fold, (idx_train, idx_valid) in enumerate(generator):
        fold_series.append(pd.Series(fold, index=idx_valid))
    fold_series = pd.concat(fold_series).sort_index()
    return fold_series

def lgb_learning(train, features):
    CFG.folds = get_groupkfold(train, CFG.target_col, 'yyyymm', CFG.num_fold)
    with mlflow.start_run(run_name=CFG.METHOD) as run:
        oof_pred = np.zeros(len(train), dtype=np.float64)
        df_importance = pd.DataFrame({'feature': features})
        df_importance['importance'] = 0
        for fold in range(CFG.num_fold):
            idx_train = CFG.folds!=fold
            idx_valid = CFG.folds==fold
            x_train = train[idx_train][features]
            y_train = train[idx_train][CFG.target_col]
            x_valid = train[idx_valid][features]
            y_valid = train[idx_valid][CFG.target_col]
            lgb_train = lgb.Dataset(x_train, y_train)
            lgb_valid = lgb.Dataset(x_valid, y_valid)
            model = lgb.train(
                params = CFG.lgb_params,
                train_set = lgb_train,
                num_boost_round = 50500,
                valid_sets = [lgb_train, lgb_valid],
                early_stopping_rounds = 100,
                verbose_eval = 100,
            )
            # model
            model_name = f'{CFG.METHOD}_fold{fold}_seed{CFG.seed}_ver{CFG.VER}'
            mlflow.sklearn.log_model(model, model_name, input_example=x_train)
            # log_metric
            pred = model.predict(x_valid)
            oof_pred[idx_valid] = pred
            score = roc_auc_score(y_valid, pred)
            mlflow.log_metric('auc', score)

            f_importance = np.array(model.feature_importance()) # 特徴量重要度の算出
            temp_importance = pd.DataFrame({'feature': features, 'importance': f_importance})
            temp_importance['importance'] = temp_importance['importance'] / np.sum(temp_importance['importance']) # 正規化
            df_importance['importance'] += temp_importance['importance']

        df_importance['importance'] = df_importance['importance'] / CFG.num_fold
        df_importance = df_importance.sort_values('importance', ascending=False) # 降順ソート
        display(df_importance.head(50))
        score = roc_auc_score(train[CFG.target_col], oof_pred)
        print(f'CV Score: {score}')

モデルの学習は,学習データへの過学習を防ぐためにクロスバリデーションすることが多く,上記のコードも学習データを5分割しております. クロスバリデーションに関しては,交差検証(cross validation/クロスバリデーション)の種類を整理してみたなどが参考になるかと思います. 現在の学習に使用しているデータは,各行を1つのデータとして扱っていることから,提出するデータと学習用データが年月で分割が行われているため,年月を分割に使用するGroup K Foldを採用しております.

また,特徴量の重要度を各モデルで計算しております. 現状の実装は各モデルで計算したもの集計し,上位50を表示しておりますが,trainデータなどのように保存したりすることももちろん可能です. 必要に応じて保存しておき,特徴量作成や特徴量選択に活用してもらえたらと思います.

spark.createDataFrame(temp_importance).write.mode("overwrite").option("mergeSchema", "true").saveAsTable("テーブル名")

モデルの学習は以下のようなコードを実行すればスタートします. また,モデルの登録はチュートリアルを参考に以下のように書けば,後から呼び出して予測に使うことができました.

# 学習データ読み込み
train= read_train_data()
# 特徴量に使う変数のリスト作成
features = [col for col in list(train) if col not in set([CFG.target_col, 'gid', 'yyyymm'])]
# lightgbmの学習
lgb_learning(train, features)

# モデルの登録
run_id = mlflow.search_runs(filter_string=f'tags.mlflow.runName = "{CFG.METHOD}"').iloc[0].run_id
for fold in range(CFG.num_fold):
    model_name = f'{CFG.METHOD}_fold{fold}_seed{CFG.seed}'
    model_uri = f"runs:/{run_id}/{model_name}"
    model_details = mlflow.register_model(model_uri=model_uri, name=f'{CFG.COMPETITION}_{model_name}')

③Predicting「予測」

予測を行い,予測結果を保存するには,以下のような実装が必要となります.

def pred_save(df):
    spark.createDataFrame(df).write.mode('overwrite').option('mergeSchema', 'true').saveAsTable(f'{CFG.METHOD}_seed{CFG.seed}_ver{CFG.VER}')
def lgb_predicting(test, features):
    x_test = test[features]
    test_preds_df = test[['gid']].copy()
    for fold in tqdm(range(CFG.num_fold)):
        model_name = f'{CFG.METHOD}_fold{fold}_seed{CFG.seed}'
        model_version_uri = f'models:/{CFG.COMPETITION}_{model_name}/{CFG.VER}'
        model = mlflow.pyfunc.load_model(model_version_uri)
        test_preds_df[f'{CFG.METHOD}_fold{fold}_seed{CFG.seed}_ver{CFG.VER}'] = model.predict(x_test)
    pred_save(test_preds_df)

登録したモデルを呼び出して,各モデルの予測を列に追加し,「pred_save」でテーブルに保存しております.

test = read_simple_test_data()
features = [col for col in list(test) if col not in set([CFG.target_col, 'gid', 'yyyymm'])]
lgb_predicting(test, features)

最後は提出ですが,予測結果を読み込み,今回は各モデルの結果を平均して提出します.

%run ../fdua-databricks-utils/utils $mode="setup_cli"
db_path = '各ユーザーごとに固有'
df = spark.table(f'{db_path}.{CFG.METHOD}_seed{CFG.seed}_ver{CFG.VER}').toPandas()
df['prob'] = df[df.columns[1:]].mean(axis=1)
# 結果の提出
submit_cli(df[['gid', 'prob']], 'my_1st_submit')

上記のコードを使用することで,特徴量などの工夫をしなくてもリーダーボードで0.9あたりは超えるかと思います.

ここからは,現状の問題点と,どのような工夫点が考えられるかをいくつか書いておきたいと思います.

現状の問題点

現状の問題点は,各行で予測を行うという方法を用いていることから,評価データ(提出データ)の過去履歴分のデータが学習用データに含まれております. 特にそれらのラベルは全て0であるため,必然的にこれらのデータで学習を行ったモデルでテストデータの予測を行うと,確率は非常に低く出てしまいます.(予測の最大値は0.2程度かと思います) それでもクロスバリデーションのスコアやリーダーボードのスコアが高めに出るのは,AUCという評価指標の特徴の1つです. AUCは,予測値のスケールに依存せず,予測値の順位情報に基づいて評価が行われるため,1.1倍や0.5などを足しても同様の結果が得られます. このような特徴から,ここまで説明してきたような雑な前処理のデータでもそれなりのスコアを出すことができております.

したがって,より良い結果を目指すためには,特徴量作成だけでなく,学習用データ,評価用データともに工夫を凝らした前処理を行い,学習に使用するデータを作成する必要があります.

また,リーダーボードスコアに相関するようなクロスバリデーションのスコアが出せるような,分割方法を検討することで,提出を行わなくても,現在のモデルや使用している特徴量の良し悪しを予測できるようになります.

第一回 金融データ活用チャレンジの戦い方(工夫点)の案

学習用,評価用データの作成方法の検討

現状の学習用データは,途中で'target_flag'に1が現れるユーザーのデータと,テストデータのユーザーの履歴から構成されているため,どのような形でモデルに入力するべきかを検討した上でデータ作成(整形)をしていく必要があると思います.

データ作成には,クロスバリデーション方法の検討なども含まれます. また,効率的にモデル学習のサイクルを回すために,データの一部(例えば20%のデータをランダムサンプリング)で実験を行ったりするなども考えられるかと思います. 作成したデータによっては,正解データのラベルが不均衡になることも考えらえるので,チュートリアルのように重み付けを行ったり,負例を減らすアンダーサンプリングなども考えられます.

特徴量作成

決定木ベースのモデルは,時系列構造などを加味することは難しいため,特徴量作成が非常に重要になってきます. 例えば,一か月前の差分(diff)を特徴量に加えたり,一か月前の情報そのまま使用する(shift)ことや,過去の特徴量の平均を計算するなども考えられます.

その他にも,いろいろな特徴量が考えられるかと思いますので,ぜひいろいろな仮説を立てて,オリジナルの特徴量を考えてスコアを伸ばしてもらえたらと思います.

モデルの検討

baselineには決定木ベースのモデルを紹介しましたが,決定木ベースのモデルにも,シンプルな決定木,ランダムフォレストなどもありますし,少しだけ結果が異なるxgbboost,lightgbm,catboostなどの勾配ブースティングモデルがあります.

また,時系列的な特徴を考慮できるNN系モデル(LSTMなど)の使用も考えられます.

2層のLSTMモデルの使用イメージ

例えば二層のLSTMモデルであれば,各月ごとのユーザー情報を入力し,入力された最後の月のラベルを予測するモデルを学習させるなど.

ただし,今回のコンペはGPU環境は準備されておらず,期限も短いため,学習させるのに時間がかかるNN系のモデルの優先順位は低いかもしれない.(NN系のモデルは特徴量のスケーリングも必要なため) まずは決定木ベースのモデルがいいのではないかと思っております.

それ以外にもロジスティック回帰やSVMなど,いろいろモデルを試してみる価値があるかもしれません.

複数のモデルの結果は,スコアを上げる方法の1つであるアンサンブルで,強力な武器となる可能性があるので,時間の許す範囲でいろいろ試してみるべきかと思います.(PyCaretとか便利かと思います.)

ハイパーパラメータチューニング

自身はあまり重視していませんが,やる価値はもちろんあります. ただ,他の工夫を優先した方が,大幅なスコア上昇が見込めるかと思いますので,時間が余ったら程度にいつもしております.

後処理

いろいろなコンペで後処理は,スコアの底上げに貢献しています.

また,複数モデルのアンサンブル,スタッキングなどもスコアの底上げに寄与することが多いです.

おわりに

学習用データに少しだけ癖(学習用データは,途中で'target_flag'に1が現れるユーザーのデータと,テストデータのユーザーの履歴から構成)がある点が,このコンペ最大の特徴かと思います.

その点に目をつぶっても,評価指標の特徴から,いろいろと工夫を凝らすことで,リーダーボードを登っていくことができるのではないかと思っております.

分析結果の共有ができないというルールの元,具体的な変数などに関してまったく触れることができていませんが,少しでも参考になれば幸いです.

また,コンペ開催期間中の質問はslackの記事のリンクを貼り付けたスレッドにしていただければ,返信できるかと思います.

この記事の内容を通して何か発想が生まれ,スコアの改善に繋がったり,学びにつながるものがあれば幸いです.

残り約一か月,どうぞよろしくお願いいたします.