たかいとの備忘録

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

LLM - Detect AI Generated Textの振り返り

はじめに

2023年11月1日(水)〜2024年1月23日(火)に開催されたLLM - Detect AI Generated Textというkaggleのコンペに参加し,参加者5,264人,4,358のチームが参加する中で単独14位の順位で終えることができました.

この記事では,主にコンペの整理と,一部ではありますが自身の取り組みを効いたかどうかに関わらず,お気持ちとともに残しておけたらと思っております.

役立つ情報は少ないかもしれませんが,何かの参考になれば幸いです.

LLM - Detect AI Generated Textはどんなコンペだったのか

このコンペはコンペ名の通り,与えられたテキストが人によって書かれたものか,生成AIによって書かれたものかを判定する精度を競う大会でした.

このコンペの最大の特徴は,提供されている学習データに生成AIによって書かれたデータが1378件中,3件しかないことかと思います.

したがって本来であれば,参加者は多額のお金を消費してChatGPTなどで自動生成されたテキストを準備したり,パワフルなGPU環境で自動生成されたテキストを準備する必要がありました. (MFゴーストのアニメが秋から始まっていたこともあり,リッチマンズレギュレーションが始まってしまうのかと思いました.)

実際には,多くの方が生成したテキストのデータセットを公開してくれたことで,この点で大きな差がついてしまうということはありませんでした. (500 LLM generated essays specifically for this competition![LLM] A "7 Prompts" training datasetなど)

もちろん,上位入賞チームでは追加してオリジナルの自動生成されたテキストを作成しているチームもありましたが,自身含めkaggleのディスカッションやデータセットで公開されているパブリックなデータのみを用いて入賞している方もいました.

このコンペのもう一つの特徴は,DAIGT V2 Train Datasetを利用した,非常に強力なLBスコアを達成することができる,TF-idfという単語の頻度情報を用いて予測を行うnotebookが公開され,それを微修正したnotebookが次々に投稿された点です.

これらのnotebookのLBは非常に強力であったものの,LBに過剰フィットしている懸念も抱えていたため,多くの参加者の悩みのタネだったのではないかと思います.

実際に多くの方がこのnotebookに振り回されて,最終順位で順位を落とす結果となり,また多くの方が公開されているLBスコアを超えることができず,脱落していったのではないかと思います.

自身の取り組み

生成テキストの準備

当たり前ではありますが,自身も如何に他の参加者と差を出すために生成テキストを様々な方法で作成しておりました.

例えば,シンプルなpromptを与えたものは公開されていたものでカバーできていたので,人手で書かれたものを入力した上で,「同じ内容,同じ単語数になるように書き換えを行ってください.」のような方法で生成したりしておりました.

しかし,こういった方法で作成したものを用いても残念ながらLBスコアに改善が見られず,不発に終わりました.

10月後半の音楽自動生成の大会で,Jackさん(@sakata_ryuji)は与えられたデータのみを使って闘うという制約のもと優勝しており,そのことを思い出し,割り切って後半はデータの生成ではなく,他のことに多くの時間を割くようにしました.

BERT系モデルの使用

だいぶ初期の時点から,頻度ベースの特徴量を用いたnotebookが注目を集めていたこともあり,多くの参加者がそちらに飛びついていたのではないかと思います. (実際に強力だったのは間違いないです.)

ここではまず,なぜ頻度ベースのnotebookが強かったかに関して,簡単にまとめます.

今回のデータは手元でモデルの学習をするとわかるのですが,すぐにCVでハイスコアに到達します.(頻度ベース,BERT系モデルのどちらでも.)

しかし,これらのモデルによる推論ではLBスコアで高いスコアが出ませんでした.(自身は,たいてい0.75~085あたりでした.)

これは,手元のデータに過学習していることを意味しており,公開ハイスコアnotebookではその対策として,テストデータに出現する単語に対象を絞って,特徴量を作成し,その特徴量を用いてnotebook内でモデルを学習させておりました.

極端な例ではありますが,手元のデータでは,「xxxx」という単語があれば生成モデルによって生成されたテキストであると判定ができるため,モデルは「xxxx」という単語の有無で判定するように学習してしまいますが,テストデータで「xxxx」という単語がほとんど出ないのであれば,このモデルはほとんど寄与しないことになります.

したがって,BERT系モデルのチューニングは,過学習対策を丁寧に行うか,テストデータに非常に類似したデータセットを構築する必要があります.

自身は後者の比重を少なくしていたため,過学習対策をしつつ,様々なモデルを学習し,LBの傾向を見ておりました.

この取り組みによって,ある程度の精度までは出せるのですが,公開LBに到達できるような精度を出すことができず,非常に困っておりました.

それでも,BERT系モデルの活用に躍起になっていたのは,頻度ベースのモデルは,あくまで単語の頻度しか見ておらず,その出現順序の情報を捨ててしまっているため,この捨てている情報分に勝機を見出していたからです.

いろいろなモデルを試している中で,fine-tuningをしなくてもそこそこのCV,LBスコアを出すことができるモデルを見つけることができました. (roberta-large-openai-detector [1])

こちらをfine-tuningしたものを単純にアンサンブルしても良かったのですが,元からそこそこのLBスコアを出すことができたため,自身は過学習を警戒して,このモデルの出力結果を特徴量として追加することにしました.

これが非常に強力で,今回の順位に大きな影響を与えました.

高速化とメモリの節約

ここまでである程度の方向は決まっていたので,あとは勾配ブースティングのモデルなどを詰め込んでいくことにしました.

その上でメモリ制約の問題や学習もnotebook内で行う関係上,高速化にも気を使いました.

一番最初に,LightGBMで単語頻度の特徴量とBERT系モデルの出力の特徴量を用いてモデルを学習させ,単語頻度の特徴量で寄与していないものを削除しました.

実際,手元の学習でもほとんど分類に寄与していない単語が大量にあったことを確認済みで,それらを除いても問題ないことは確認済みでした.

このデータ削減によって,xgboostなどのモデルを,GPUで高速に学習させることができました.

Pseudo Labeling(擬似ラベリング)

実は蓋を開ければPrivateスコアにそこまで大きなインパクトはなく,なくても十分金圏に入るスコアが出ておりました.

ただ,今回のような特殊な状況になっているコンペにおいて,こちらは最終subを選ぶ上でかなり参考になりました.

というのも,LBを計算するのに使用しているデータに過剰にフィッティングし,最終順位のスコアを決めるのに使用しているデータが散々な結果になっている場合,Pseudo Labelingを行うことで著しくLBスコアが悪化する可能性が高くなります.

しかし,実際にはLBスコアは悪化することなく,ある程度は最終順位のスコアを決めるデータにもフィッティングしていることに確信を持つことができました.

また,ちょっとした工夫ではありますが,テストデータは5つのトピックで構成されており,トピックの出現頻度や分類の難易度によってモデルが重視しているトピックに偏りが生じ,単純なスコアの上位,下位を取得してしまうとトピックの割合に偏りが生じることが懸念されたので,各トピックごとに,上位,下位のものを取得し,疑似ラベルの割り当てを行い,学習するのに追加しました.

感想

最終的な提出物はかなりシンプルな方法になっておりますが,ここに書ききれない大量の試行錯誤手元のデータを用いた様々な分析の賜物でもあります.

正直,公開noteook強すぎて,心が折れかけている局面も多かったですが,これまで学んだことを一つ一つ検証しながら,最後まで粘ったことと,多少の運が味方してくれたことにより,今回の成果につながったと思っております.

これまで参加してきた多くのコンペでディスカッションを読ませていただき,またコンペ終了後には多くの方がsolutionを公開してくれているおかげで,自身も成長できました.

また,対面でのイベントでもこれまで多くの方とディスカッションさせていただき,また応援していると励ましの言葉をいただいたおかげで,ようやく一つの目標としていたソロ金に到達することができました.

ただ,今回publicでは良い結果が出せなかったこと,まだ上位とは大きなスコア差が残っていることは事実であり,まだまだ不足している能力ばかりなので,引き続きコンペに参加しながら,引き出しを増やしていくことができたらと思います. (理想としては,力の入れたコンペでは50位あたりに当たり前のように入れるようになりたいなと思っております.)

また,自身がそうであったように,今後始める方に向けて積極的にディスカッションへ投稿したり,solutionなども公開していけたらと思います.

おわりに

自身が今日までデータ分析コンペを続けることができたのは,たくさんの方の応援があったからです.

一部の天才を除き,「継続は力なり」で継続して少しずつでも成長し続けることが大事だと思いますので,今後も継続していけたらと思います.

また,多くの方が参加しているからこそ学びが多いと思っておりますので,今後も皆様とコンペで切磋琢磨できるのを楽しみにしております!

モチベーション低下時には,以下の記事を見返すようにしているので,皆様もモチベーションが低下したときにご活用ください. takaito0423.hatenablog.com

それでは良いコンペライフを!

[1] Solaiman, I., Brundage, M., Clark, J., Askell, A., Herbert-Voss, A., Wu, J., … & Wang, J. (2019). Release strategies and the social impacts of language models. arXiv preprint arXiv:1908.09203.

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

はじめに

第二回 金融データ活用チャレンジに関しては,以下をご参考ください. signate.jp

第一回金融データ活用チャレンジに続き,今回もbaselineとなるようなnotebookを共有することができたらと思います.

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

当該コンペはディスカッション(フォーラム)がなく,baselineコードなどの共有が少ないため,コードを共有できたらと思います. また,ルールに書いてあるように,前処理,学習,予測の3つにコードを分け実装が必要となります.

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

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

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

今回のコンペは第一回よりもよりシンプルな課題設計となっており,よりトライしやすいコンペだと感じました.

また,分析環境やツールも充実しておりますが,使い慣れていない環境は最初に時間をだいぶ使うと思うので,使うものをいくつか絞るといいのではないかと思います.

ただ,コンペが終わると使えなくなってしまうので,コンペが終わってからも使えるcolab環境のコードを共有することができたらと思います. 今後,データ分析を個人的にするときなどにも,ぜひ活用していただければと思います!

Baseline Notebook

Baseline Notebookを以下で共有します.

colab.research.google.com

「ファイル」から「ドライブにコピーを保存」を選んでもらい,コピーすればほとんどそのまま使えると思います. signateからコンペのデータをダウンロードし,自身のドライブに保存してください. また,データの置く場所によって,データへのパスなどを書き換えてもらえたらと思います.

上記のNotebookを使用することで,特徴量などの工夫をしなくてもリーダーボードで0.682(現在90位)くらいのスコアが出ます.

ここからは,公開したものに関して簡単に紹介します.

作成が必要な関数

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

①Preprocessing「前処理」

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

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

 ②Learning「学習」

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

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

 ③Predicting「予測」

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

まずはこれらに沿った関数を紹介します.

①Preprocessing「前処理」

def Preprocessing(input_df: pd.DataFrame()) -> pd.DataFrame():
    def deal_missing(input_df: pd.DataFrame()) -> pd.DataFrame():
        output_df = input_df.copy()
        for col in ['RevLineCr', 'LowDoc', 'BankState', 'DisbursementDate']:
            output_df[col] = input_df[col].fillna('[UNK]')
        return output_df
    def clean_money(input_df: pd.DataFrame()) -> pd.DataFrame():
        output_df = input_df.copy()
        for col in ['DisbursementGross', 'GrAppv', 'SBA_Appv']:
            output_df[col] = input_df[col].str[1:].str.replace(',', '').str.replace(' ', '').astype(float)
        return output_df
    output_df = deal_missing(input_df)
    output_df = clean_money(output_df)
    output_df['NewExist'] = np.where(input_df['NewExist'] == 1, 1, 0)
    def make_features(input_df: pd.DataFrame()) -> pd.DataFrame():
        output_df = input_df.copy()
        # いろいろ特徴量作成を追加する
        return output_df
    output_df = make_features(output_df)
    return output_df

関数「Preprocessing」の中の「make_features」という関数内に,特徴量作成をいろいろと追加してもらえたらと思います. いろいろな工夫を加えるのがコンペの醍醐味の一つかと思いますので,何も特徴量作成は行わず,最低限のデータ処理のみを行ったものとなります.

カテゴリカル変数のラベルへの変換やカウントエンコーディング(学習データ内で何回出現したかで埋め込みを行う)も本来この中に入れるべきなのですが,学習データ時とテストデータ時で分岐が必要で,煩雑になるため,今回は別枠で実施しております.

②Learning「学習」

baselineとして,kaggleなどのコンペで使用率の高い決定木ベースの手法の実装を共有します.

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

# ====================================================
# Metric
# ====================================================
# f1_score

# ====================================================
# LightGBM Metric
# ====================================================
def lgb_metric(y_pred, y_true):
    y_true = y_true.get_label()
    return 'f1score', f1_score(y_true, np.where(y_pred >= 0.5, 1, 0), average='macro'), CFG.metric_maximize_flag

# ====================================================
# XGBoost Metric
# ====================================================
def xgb_metric(y_pred, y_true):
    y_true = y_true.get_label()
    return 'f1score', f1_score(y_true, np.where(y_pred >= 0.5, 1, 0), average='macro')


def lightgbm_training(x_train: pd.DataFrame, y_train: pd.DataFrame, x_valid: pd.DataFrame, y_valid: pd.DataFrame, features: list, categorical_features: list):
    lgb_train = lgb.Dataset(x_train, y_train, categorical_feature=categorical_features)
    lgb_valid = lgb.Dataset(x_valid, y_valid, categorical_feature=categorical_features)
    model = lgb.train(
                params = CFG.classification_lgb_params,
                train_set = lgb_train,
                num_boost_round = CFG.num_boost_round,
                valid_sets = [lgb_train, lgb_valid],
                feval = lgb_metric,
                callbacks=[lgb.early_stopping(stopping_rounds=CFG.early_stopping_round, 
                                              verbose=CFG.verbose)]
            )
    # Predict validation
    valid_pred = model.predict(x_valid)
    return model, valid_pred
def xgboost_training(x_train: pd.DataFrame, y_train: pd.DataFrame, x_valid: pd.DataFrame, y_valid: pd.DataFrame, features: list, categorical_features: list):
    xgb_train = xgb.DMatrix(data=x_train, label=y_train)
    xgb_valid = xgb.DMatrix(data=x_valid, label=y_valid)
    model = xgb.train(
                CFG.classification_xgb_params, 
                dtrain = xgb_train, 
                num_boost_round = CFG.num_boost_round, 
                evals = [(xgb_train, 'train'), (xgb_valid, 'eval')], 
                early_stopping_rounds = CFG.early_stopping_round, 
                verbose_eval = CFG.verbose,
                feval = xgb_metric, 
                maximize = CFG.metric_maximize_flag, 
            )
    # Predict validation
    valid_pred = model.predict(xgb.DMatrix(x_valid))
    return model, valid_pred
def catboost_training(x_train: pd.DataFrame, y_train: pd.DataFrame, x_valid: pd.DataFrame, y_valid: pd.DataFrame, features: list, categorical_features: list):
    cat_train = Pool(data=x_train, label=y_train, cat_features=categorical_features)
    cat_valid = Pool(data=x_valid, label=y_valid, cat_features=categorical_features)
    model = CatBoostClassifier(**CFG.classification_cat_params)
    model.fit(cat_train, 
              eval_set = [cat_valid],
              early_stopping_rounds = CFG.early_stopping_round, 
              verbose = CFG.verbose, 
              use_best_model = True)
    # Predict validation
    valid_pred = model.predict_proba(x_valid)[:, 1]
    return model, valid_pred

def gradient_boosting_model_cv_training(method: str, train_df: pd.DataFrame, features: list, categorical_features: list):
    # Create a numpy array to store out of folds predictions
    oof_predictions = np.zeros(len(train_df))
    oof_fold = np.zeros(len(train_df))
    kfold = KFold(n_splits=CFG.n_folds, shuffle=True, random_state=CFG.seed)
    for fold, (train_index, valid_index) in enumerate(kfold.split(train_df)):
        print('-'*50)
        print(f'{method} training fold {fold+1}')
        
        x_train = train_df[features].iloc[train_index]
        y_train = train_df[CFG.target_col].iloc[train_index]
        x_valid = train_df[features].iloc[valid_index]
        y_valid = train_df[CFG.target_col].iloc[valid_index]
        if method == 'lightgbm':
            model, valid_pred = lightgbm_training(x_train, y_train, x_valid, y_valid, features, categorical_features)
        if method == 'xgboost':
            model, valid_pred = xgboost_training(x_train, y_train, x_valid, y_valid, features, categorical_features)
        if method == 'catboost':
            model, valid_pred = catboost_training(x_train, y_train, x_valid, y_valid, features, categorical_features)
        
        # Save best model
        pickle.dump(model, open(CFG.MODEL_DATA_PATH / f'{method}_fold{fold + 1}_seed{CFG.seed}_ver{CFG.VER}.pkl', 'wb'))
        # Add to out of folds array
        oof_predictions[valid_index] = valid_pred
        oof_fold[valid_index] = fold + 1
        del x_train, x_valid, y_train, y_valid, model, valid_pred
        gc.collect()

    # Compute out of folds metric
    score = f1_score(train_df[CFG.target_col], oof_predictions >= 0.5, average='macro')
    print(f'{method} our out of folds CV f1score is {score}')
    # Create a dataframe to store out of folds predictions
    oof_df = pd.DataFrame({CFG.target_col: train_df[CFG.target_col], f'{method}_prediction': oof_predictions, 'fold': oof_fold})
    oof_df.to_csv(CFG.OOF_DATA_PATH / f'oof_{method}_seed{CFG.seed}_ver{CFG.VER}.csv', index = False)
    
def Learning(input_df: pd.DataFrame, features: list, categorical_features: list):
    for method in CFG.METHOD_LIST:
        gradient_boosting_model_cv_training(method, input_df, features, categorical_features)

モデルの学習は,学習データへの過学習を防ぐためにクロスバリデーションすることが多く,上記のコードも学習データを7分割しております. クロスバリデーションに関しては,交差検証(cross validation/クロスバリデーション)の種類を整理してみたなどが参考になるかと思います. 今回のデータは,学習データとテストデータの分布が非常に似ているため,ランダムに分割を行っておりますが,目的変数のラベル割合を同じように分割する方法などもあるので,いろいろと試してみるといいかと思います.

また,lightgbmとxgboostは,学習の過学習抑制のためのearly stoppingに使用する評価指標をf1 scoreで止めることができるようにしております.

③Predicting「予測」

def lightgbm_inference(x_test: pd.DataFrame):
    test_pred = np.zeros(len(x_test))
    for fold in range(CFG.n_folds):
        model = pickle.load(open(CFG.MODEL_DATA_PATH / f'lightgbm_fold{fold + 1}_seed{CFG.seed}_ver{CFG.VER}.pkl', 'rb'))
        # Predict
        pred = model.predict(x_test)
        test_pred += pred
    return test_pred / CFG.n_folds
def xgboost_inference(x_test: pd.DataFrame):
    test_pred = np.zeros(len(x_test))
    for fold in range(CFG.n_folds):
        model = pickle.load(open(CFG.MODEL_DATA_PATH / f'xgboost_fold{fold + 1}_seed{CFG.seed}_ver{CFG.VER}.pkl', 'rb'))
        # Predict
        pred = model.predict(xgb.DMatrix(x_test))
        test_pred += pred
    return test_pred / CFG.n_folds
    
def catboost_inference(x_test: pd.DataFrame):
    test_pred = np.zeros(len(x_test))
    for fold in range(CFG.n_folds):
        model = pickle.load(open(CFG.MODEL_DATA_PATH / f'catboost_fold{fold + 1}_seed{CFG.seed}_ver{CFG.VER}.pkl', 'rb'))
        # Predict
        pred = model.predict_proba(x_test)[:, 1]
        test_pred += pred
    return test_pred / CFG.n_folds

def gradient_boosting_model_inference(method: str, test_df: pd.DataFrame, features: list, categorical_features: list):
    x_test = test_df[features]
    if method == 'lightgbm':
        test_pred = lightgbm_inference(x_test)
    if method == 'xgboost':
        test_pred = xgboost_inference(x_test)
    if method == 'catboost':
        test_pred = catboost_inference(x_test)
    return test_pred

def Predicting(input_df: pd.DataFrame, features: list, categorical_features: list):
    output_df = input_df.copy()
    output_df['pred_prob'] = 0
    for method in CFG.METHOD_LIST:
        output_df[f'{method}_pred_prob'] = gradient_boosting_model_inference(method, input_df, features, categorical_features)
        output_df['pred_prob'] += CFG.model_weight_dict[method] * output_df[f'{method}_pred_prob'] 
    return output_df

その他の補足

④Postprocessing「後処理」

今回の評価指標は,後処理が非常に重要となります. 詳しくは,記事の後半で記載しますが,ここではモデル学習時に切り分けた評価データの予測値を用いて,最適な閾値を計算し,その閾値を用いて,0, 1の割り当てを行うコードを共有します.

重要特徴量の確認

model = pickle.load(open(CFG.MODEL_DATA_PATH / f'lightgbm_fold1_seed42_ver1.pkl', 'rb'))
importance_df = pd.DataFrame(model.feature_importance(), index=features, columns=['importance'])
importance_df['importance'] = importance_df['importance'] / np.sum(importance_df['importance'])
importance_df.sort_values('importance', ascending=False)

lightgbmのモデルにおける特徴量の重要度を確認することができます.

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

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

特徴量作成

特徴量作成が非常に重要になってきます. 時間に関する特徴量などは,今回使えていないので,まずはこのあたりから始めてみてはいかがでしょうか.

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

また,共有した特徴量の重要度を確認しながら,どの特徴量に着目して,特徴量作成を行ったりしていくと効率がいいかもしれません.

モデルの検討

今回のベースラインでは決定木ベースのモデルとして,lightgbm,xgbboost,catboostを紹介しました.

前回と異なり,今回は環境の制約がありませんので,GPU環境でNN系のモデルを試してみる価値があるかもしれません. また,GPU環境がなくとも,データサイズが小さいので,シンプルなNNのモデルであれば,CPU環境でも学習が可能かと思います. なお,NN系のモデルは,決定木ベースのモデルと異なり,特徴量のスケーリングが必要であるため,前処理の追加が必要となることに注意が必要です.

それ以外にもロジスティック回帰やSVMなど,いろいろモデルを試してみる価値があるかもしれません. 複数のモデルの結果は,スコアを上げる方法の1つであるアンサンブルで,強力な武器となる可能性があるので,時間の許す範囲でいろいろ試してみるべきかと思います.(PyCaretとか便利かと思います.)

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

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

後処理

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

第一回の評価指標はAUCでしたが,ここが前回との大きな違いになるかと思います.

今回の評価指標のF1-scoreは,予測を0, 1のいずれかに割り当てる必要があります.

今回の評価指標に関しては,y-Carbonさんの書かれた記事である「なぜ分類問題のLBでLogLossではなくF1を使うか」が大変勉強になるかと思います.

モデルによっては,0, 1を返すモデルもありますが,多くの出力は0から1の連続値を取ります.

したがって,0, 1を決めるために閾値を決める必要があります.

今回共有したnotebookでは,学習時のOut-Of-Foldの予測した値を元に,ベストな閾値を推定するようにしております.(この後処理の有無でLBのスコアが0.01ほど変わりました.)

後処理もいろいろと工夫の余地があるかと思います!

おわりに

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

追記(20240218)

コンペも終わり,共有したコードの順位を確認したところ,313位に入れるようでした.

また,ご丁寧にSNSなどでリプライやDMなどでお礼の連絡をいただき,少しでも参考になったようで大変嬉しく思います.

この界隈はスキルの共有に関して,とてもオープンなところがいいことだと思っておりますので,今後も続けていけたらと思います.

本題ですが,他の参加者のSolutionなどを踏まえた上で,3点追記することができたらと思います.

評価指標と閾値に関しての補足

今回の評価指標のF1-scoreは,予測を0, 1のいずれかに割り当てる必要があると述べた通り,多くの参加者が閾値をどう決めるを一度は考えていたかと思います.

ここで自身は,oof(Out-Of-Fold)の出力を利用して最適な閾値を決める方法を共有しました. 時間がなかったことから,いろいろと説明を省いてしまったことで,他の参加者の共有に関しても前提などの言及がないまま,記事にまとめているものがほとんどでしたので,この点をまず補足します.

まず,この方法を利用するにあたって前提となるのが,学習データとテスト(評価)データの分布がある程度一致している必要があります.

コンペのルール的には,テスト(評価)データの分布と学習データの分布を可視化することは禁止されていないため,多くの参加者と同様に自身も分布がほぼ一致していることを確認したことで,上記の方法を採用しておりました.

したがって,必ずしも提案している方法がベストではなく,学習データとテストデータの分布が一致していないデータ分析コンペティションや実務での状況は多数存在し,その場合にはまた異なるアプローチを考える必要があります.

特に,実務においては将来のデータを実際に除くことができないため,データの分布が変化しないかどうかを検証するところから始める必要があり,実際に変化する場合には様々な工夫が必要となります.

こういった前提を考慮できないと,実務で役に立たないと言われかねないので,補足させていただきました.

次に,こちらは他の方のsolutionやslackでのディスカッションにもありましたが,foldごとにベストな閾値を算出しておき,各foldのテストデータの予測値を平均するのではなく,各foldごとに0,1の判定を行い,多数決で最終的な0,1を決める方法もあります.(こちらは他の人のsolutionなどを参考にしてもらえればと思います.)

最後に,今回の評価指標を上げるためには,閾値付近のデータの分離が非常に重要となります.

一般的にloglossなどは,より1は1側に,0は0側に近づけることを重視しますが,今回のようにある閾値で0と1に分離することを考えると,1を0.9999...に近づけることはそこまで重要ではなく,仮に0.95くらいでも十分です.

極端な話をすれば,仮に予測値が0.4と0.6の値しかとらない場合,0,1をしっかり分離できるのであれば,F1-scoreは向上します.(loglossはあまり良くなくても)

同様にAUCなども,本来1であるデータの予測値が0.85で本来0であるデータの予測値が0.95の場合,少し悪化しますが0.6~0.7あたりで閾値を決める場合,F1-scoreには何の影響も与えません.

このような評価指標の特徴を考慮することも,非常に重要だと思っております.

評価指標と今回のデータの特徴を踏まえて行った工夫

上記の評価指標の特徴を踏まえた上で,一つ試してみる価値がありそうなのは,複数seedにおいて,oofの予測が0になったり1になったりするデータをきちんと分類できることが重要であり,これらのデータの重みを学習時に大きくすることにより,CV, LBが改善する可能性があります.

また,逆に今回のデータは生成データであり,一定の割合でノイズとなる予測不可能なデータが含まれています.

これは何かしらの方法で,確率を計算し,乱数生成によりラベルを割り当てていると考えると,0.95の可能性で1だけど0が割り当てられているデータや,その逆もまた考えられます.

したがって,逆に複数seedで必ず正解できないようなデータは,学習を阻害している(ノイズとなっている)可能性があるため,weightを小さくすることで,CV, LBが改善する可能性がありました.

詳細な分析はLate Subができてないのでわかりませんが, CV: 0.6801, LB: 0.6897342, Private: 0.6817465 CV: 0.6874, LB: 0.6903327, Private: 0.6837645 と自身の比較では,すべて向上しておりました.

LBとPrivateの関係

今回,最終日の夕方からサブと最終サブの選択ができなかったことで,ボードが荒れてしまったこともありますが,最終評価とそのサブの暫定評価の結果は上位350位のものをサンプリングすると以下のようになっておりました.

FDUA2 散布図

この結果を考慮すると,上位数チームを除いて300位くらいまでの順位はかなり運要素の強いものになっている可能性があるため,順位に一喜一憂する必要はまったくないように思います.(そもそも最終サブを数時間前から選べない時点で順位に意味がないという意見もあるかと思いますが.)

ぜひ,順位にとらわれることなく,解法を簡単にでもいいので共有してもらえると嬉しく思います. 追記の最後まで読んでいただきありがとうございました!

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

2023年振り返り

はじめに

今年も本当にあっという間の一年でした.

最近は一週間以上前のことを思い出すのも大変になってきたので,将来の自分のためにも毎年恒例の振り返りをしておきたいと思います.

参加していたコンペ

kaggle

ソロ銀×1,ソロ銅×9でした.

これまでのコンペ参加から得られた経験を元に,最低限の試した方がいい工夫を,公開されているnotebookや自身の過去のコードに追加することで,baselineを超えるスコアは出せるようになったものの,そこから新たな応用などを行うことで,周りと差分を出していく部分がほとんどできておらず,金圏とは無縁の一年でした.

また,コンペ終了後に上位者の解法を読んで,なるほどと理解できるレベルあたりまでしかやり込めていなかったものがほとんどでした.

来年は一つだけでもいいので,最後まで精一杯やり切ることを一つの目標にしたいと思います.

個人的な変化としては,画像系のコンペにも挑戦しました. また,ICRでNotebookで初めて金のメダルをいただきました. ICRはNotebookのメダルだけでなく,公開されている後処理は,LB向上に偶然寄与しているだけであり,最終スコアではむしろスコアが悪化する可能性が非常に高いことに,手元のCVで気づいたので,discussionも投稿しました. 実際,公開されている後処理は最終スコアを悪化させており,それらをそのまま適用した参加者は順位が悪化していました. このあたりは,運要素の強かったコンペだったこともあり,最終的な順位は何とも言えませんが,多少の成長を感じることができました.

英語が苦手なこともあり,あまりNotebookやdiscussionを投稿できていませんが,今後も無理のない範囲で,投稿していくことができたらと思います.

kaggle以外

kaggle以外も幅広く参加しておりました.

今年のatmaCupは惨敗でしたので,来年はリベンジしたいところです.

金融データ活用チャレンジは,来年1月から第2回が開催されるようです.

その他の活動

AIによるアドリブメロディ生成を行う大会である 弁財天 に参加しました.

また,秋には関東Kaggler会に参加しLT(ライトニングトーク)を行いました.(関東kaggler会に関しては「【第1回】関東Kaggler会の参加記」にまとめております.)

国内学会が中心ですが,学会での発表5件,査読付き論文の執筆を行いました. また,今年から査読の依頼も引き受けるようになり,微力ではありますが,分野に貢献することができました.

あとは,出身研究室が10周年だったのでOB・OG会を企画し,開催できたのもとても良かったなと思っております.

総括

少しずつではありますが,活動の範囲を広げることができました. それゆえに,一球入魂することができていないのも事実なので,そのあたりのバランスを考えていきたいと思います.(来年こそはkaggleでソロ金をとりたいです...)

今年の目標をいくつか立てていたのですが,「ブログ記事の執筆」,「多くの人と関わる」,「研究活動に力を入れる」あたりは及第点かと思います.

とはいえ,学会,イベント後の懇親会,オフラインでのイベントにも,例年に比べて積極的に参加したことで,多くの方とお話しさせていただくことができ,楽しい一年になりました. 挙げ始めたらきりがありませんが,第1回 金融データ活用チャレンジ の懇親会,yukiCup 2023 Summerオルタナティブデータ分析チャレンジ 2023の最終審査会,弁財天など,本当にいろいろとお世話になりました. 来年も,前年対比でオフラインのイベント参加回数を増やしていけたらと思いますので,ぜひ見かけた際には気軽にお声がけいただけますと幸いです.

ご飯とかのお誘いもお待ちしております!

あとは,休みという休みを作るのが下手なので,2ヵ月に1回はオフの日を作るのを来年の目標にできたらと思います.

おわりに

今年もたくさんの人にお世話になりました. 来年もどうぞよろしくお願いいたします.

【第1回】関東Kaggler会の参加レポート

はじめに

2023年9月23日(土)に開催されていた【第1回】関東Kaggler会に参加してきました.

正式なイベント名は関東Kaggler会ですが,「第1回」をあえてつけているのは「第2回」以降への期待からです.

個人的な備忘録ですが,次回以降の開催時の参考になったりすれば幸いです.

関東Kaggler会に関して

  • 会場: KDDI DIGITAL GATE
  • 参加者: 現地90人くらい?オンライン382人 (zoom枠290+Youtube枠92)
  • 招待講演5件,スポンサーセッション3件,LT (ライトニングトーク)6件,懇親会

概要の抜粋

Kaggerを対象にしたオフラインイベントを開催します!本イベントでは、Kaggle大国である日本の中でもトップクラスの結果を出されているKagglerの方々が経験や解法を共有してくださいます!

Kaggle好きが集まって楽しむ会

Kaggler同士の交流を促進し、コミュニティーをさらに発展させる会

ハッシュタグは #kanto_kaggler

コロナ流行後,しばらく対面でのこの規模の関東でのイベントは自身の知る範囲ではなかったので,非常に楽しみにしておりました.

参加のモチベーション

参加のモチベーションはたくさんありました.

まず,普段同じコンペに参加し,ディスカッションなどで交流している人や勉強になる公開notebook,解法の共有などをしてくれている人に直接お会いし,感謝を伝えることでした.

また,普段からお世話になっている人たちとも,自身の性格上,なかなかお会いする機会が少ないので,現地参加できたらと思っておりました.

あとはどうしても惰性でコンペに参加しがちになっていたので,良い刺激をもらえるようにとも思っておりました.

LT登壇の理由

LTの申し込みを行ったのには,3つの理由がありました.

まず,上記の参加モチベーションを達成するためには現地参加する必要がありましたが,当初は申し込み人数と現地参加の許容人数から現地参加倍率が2倍近くになっておりました.

説明をよく見るとLT登壇が決まれば,優先して現地参加が可能であると書いてあったことが,理由の1つ目です.

2つ目の理由は,自身が初対面の人に対して声をかけに行くのがとても苦手であり,せっかく参加しても多くの人と交流できない可能性が高かったので登壇できたらと思っておりました.

知名度や実力的にも探しに来てくれるほどでもないので,自身から登壇することで話しかけに来てくれる人が何人かいれば,懇親会でボッチで過ごす時間を限りなく0に近づけることができるのではないかと目論んでいました.(なお,参加していたkagglerはどなたも温かい人ばかりで,自身が見渡す限り,懇親会でボッチで過ごしている人はいませんでした!)

3つ目の理由は,この数年間でデータ分析コンペからフェードアウトしていってしまった人が自身の観測できる範囲でも一定数いることや,自身のように銀圏~銅圏あたりから上に伸び悩んでいる多くのkagglerがきっといるだろうと思い,そういった人たちと思いを共感しつつ,無理のないペースで楽しみながらデータ分析コンペを続けて行きましょうという思いを伝えたかったので,登壇したいと思っておりました.

今回自身は,「データ分析コンペとの向き合い方」というタイトルでLTをさせていただきましたが,この内容は一番最初から考えていたものでした.

ただ,イベントの内容や現地参加予定の錚々たる顔ぶれから,この内容で本当にいいのかなどの葛藤からいくつかネタを考えていました.

数日考えた結果,登壇者全員が高度な技術やテクニックの話をすることは,今後の第2回以降の登壇のハードルを上げ,初学者が現地参加しにくくなってしまうのが一番良くないなと思い,技術面一切なしのこのテーマでLTの申し込みをさせていただきました. (会場は金圏入賞者が多かったので,オンラインで聞いてる方の数人にでも刺さればと思っておりましたが,現地参加の方からも温かいフィードバックを多くいただくことができ,ホッとしております.)

余談ですが聞いた話によると,参加者があまりにも豪華だからという理由で現地参加を辞めてしまった方が今回もいたと伺っており,そういった壁は取り除いていきたいなと思っています.

資料はこちら

当日の現地の様子

午前中予定があったため,開催ギリギリに到着したこともあり,ほとんど周りの人と話すことなくオープニングが始まりました.

前半

kaggle歴13年で金を20枚保有しているsmlyさんから始まり,solo金4枚持ちのMakotuさんと招待講演が続きました.

この時点で,想定していたよりもずっと講演内容が充実しており,つよつよな人たちがしっかりと会の質を高めてくれていたおかげで,自身がLTを行う意味が多少はありそうだという気持ちと,果たしてこのレベル感の中に技術に関してまったく言及しないLTをして大丈夫なのかという気持ちの両方が溢れておりました.

Makotuさんの「コンペ選択時の自分の脳内振り返り」のスライドは,自身も入れるか悩んでいたスライドでした. (なお,自身は銀複数所持までをより細分化したものしか今は作れないので断念しましたが,いつかは金5枚以上保持まで到達し,お気持ちの変化を共有できるようになりたいものです.)

その後の最初の休憩はお手洗いと飲み物を買いに行ってあっという間に終了してしまいました.

中盤

休憩後はスポンサーセッションが続き,どこの会社もとても魅力的な環境だなと感じました.(業務内容や事業内容,制度や目標など)

続いて,kaggle歴2年弱でソロ優勝を達成したゆめねこさん,金を10枚保有しているsenkinさんと続きました.

ゆめねこさんは,自身も取り組んでいたBenetechコンペに関して解法を紹介してくれましたが,試行錯誤の多さに貫録を感じました.

特に,データ追加の部分の500枚や10000枚のアノテーションデータ作成(モデルで推論して修正する工夫などを含む)は,才能だけでなく相当な労力を割いており,勝つべくして勝っているという印象を受けました.(このあたり,いい感じにインフレ感があってとても良かったです)

senkinさんは,中国での界隈の状況の報告から始まり,ここ数年参加していたコンペに関して共有がありました.

大量にある予測対象を主成分分析で削減し,逆変換するというのは,引き出しになかったのでとても参考になりました.

その後の二回目の休憩は時間もおして短かったことや,ちょっとしたトラブルで資料共有などしていたら終わっていました.

終盤

最後の招待講演は,金圏常連のcharmさんでした.

わかっていたことではありますが,招待講演はどれも質が高くメンバーも豪華で,自身のLTのバイアス部分への振りはほぼ完成していました.

さらに,charmさんの講演の中でknshnbさんの話(初のコンペで優勝)が出てきて,準備は整った感がありました.

charmさんの講演の最後は,自身のスライドと多少内容が被る部分もあり少しヒヤヒヤしてましたが,もう自身もスライド変更することできないので,立っている場所が異なるし,何とかなるだろうの精神でした.

LT

他のLT全てが技術的な話だったらもっと緊張していたと思いますが,yukiさんが「yukiCup 2023 Summer開催報告」をしてくれたおかげでだいぶ気が楽でした.(本当にありがとうございます!)

yukiCup 2023 Autumn x atmaCupの受付が始まったのでぜひ!

自身に関しては,目が悪くあまり自身のスライドの文字も見えなかったので,スライド見ながら日頃思っていることを話すようにしました.

会場は横に広く,暗かったこともあり,現地のリアクションがあまり感じとれなかったので,内心はドキドキでした(汗)

時間の制約で全体的に説明不足ではありますが,伝えたいことは最低限伝えられて良かったです!

懇親会

多くの人に声をかけていただき,またLTに対して温かい言葉をいただき,ありがとうございました.

個人的にはもう少しだけ,懇親会の時間が長くても嬉しいなと思いました.(話したいと思っていたけど話せなかった人も多数いたので)

その後

貴重な機会を作ってくれた運営の皆様に少しでも貢献できればと思い,会場の後片づけのお手伝いをさせていただきました.

二次会に関しては,この熱量が冷めないうちに帰宅して実装がしたかったので,今回は参加しませんでしたが,次回はぜひ参加したいと思っています!

感想

いつもLBの下の方から金圏を眺めておりますが,やはり上位争いしている方々に関して,才能もあると思いますが大量の試行錯誤も行っており,本当に頭が上がりません.

上位の方々の手法に関しては,ソリューションなどで読むことができますが,考え方や取り組みなどを聞くことができ大変良い刺激を受けることができました.

やはり強い人たちが,全力でやっている中で上位に入ることに意味があり,自身もいつかそっち側に行けるように,自己研鑽を続けていきたいと改めて強く思いました.

LTに関しても,蓋を開けてみたらやって本当に良かったなと思いました.

会場の参加者にも思っていたよりも響いた方が多く,自身は登壇が不慣れでハッシュタグを追えてなかったのですが,そちらでも多くのコメントいただけて大変嬉しく思います.

(違う発表に対してのコメントや見落としがあったら教えてください(追加や削除します))

伝えたかったことを改めて本当に簡潔にまとめると,「①バイアスがあることを認識し ②目的をもって ③仲間と切磋琢磨しながら ④無理のないペースで,今後もデータ分析コンペを楽しんで行きましょう!」というものです.

氷山の一角

下を見て安心していいとは思っておりませんが,上だけを見るのでなく自身の立ち位置を正しく理解し,謙虚に研鑽を積むことが非常に重要だと思っています.

また,自身は皆さんと一緒にコンペに参加できるのを楽しみにしております!

ぜひ親近感を持ってくれた方は,気軽にお声がけしてくれると嬉しいです.

少しずつではありますが,自身がそうだったように,参考になるようなコードやディスカッションなども共有し,コミュニティーの活性化などにも貢献することができたらと思います.

個人ではという形になってしまうかと思いますが,第2回の開催など,お手伝いできることがあれば,協力できたらと思っております!

改めて,企画・運営,登壇者,参加者の皆様,ありがとうございました!

今後もどうぞよろしくお願いいたします.

JSAI2023参加レポート

はじめに

2023年6月6日(火)〜9日(金)に開催されていた「人工知能学会全国大会」に参加しておりました.

注意事項として本記事は研究などに関してほとんど触れません.

ものすごく雑多かつ個人的な備忘録となります.

JSAI2023に関して

  • 会場: 熊本城ホール(熊本県熊本市)
  • 開催期間: 2023年6月6日(火)〜9日(金)
  • 参加者: 3000人越え(現地2000人越え)

去年の京都での開催からあっという間に一年が経過し,今年は熊本での開催でした.(来年は静岡!)

現地参加が非常に多く,セッションによっては立ち見も発生しておりました.

参加モチベーション

  • いろいろな人とのディスカッションやコミュニケーション
  • 共同研究・インターンの宣伝
  • 多種多様な研究分野の動向調査

まず何といっても,いろいろな人と話すことができるのが,最大のモチベーションとなっております.

セッションの発表には質問時間が確保されていますが時間も限られているので,セッションが終わった後にいろいろ話せるのは非常にありがたいです.

特に,実務の観点での議論やうまくいかなかった原稿に記載のない取り組みなどに関して情報を交換できるのは,とても勉強になっております.

今回もたくさんの方といろいろとお話しをすることができました.

前回のJSAI2022や3月に行われたNLP2023で,すでに名刺をお渡しさせていただいていたので,今回はそんなに名刺は減らないのではと思っておりましたが,100枚準備しておいた名刺は残り数枚になりました.

最終日には少しでも声をかけやすいようにと自身の写真をtweetした(すでに削除済み)こともあり,kaggle界隈の方々とも多くお話しさせていただくことができ,非常に嬉しかったです!

Turingのころんびあさんと話していたのですが,企業ブースは学生さんと接点を作るのに非常に適しているなと改めて思いました.

インターンなどに興味がある学生さんが自然と会場に集まるので,学生にとっても企業にとってもいい交流の場だと思っております.

熊本ご飯

tabelog.com

tabelog.com

西銀 ぼたん

tabelog.com (hakubishin3さんのツイートを見ていきましたが,とても美味しかったです!)

秀ちゃんラーメン

tabelog.com (タクシーの運転手にオススメされ,u++さんのツイートも見ていきましたが,こちらもとても美味しかったです!)

黒亭

セッションや発表

プレナリーセッションはいろいろなことを幅広く知ることができ,とても面白かったです.

発表は挙げるときりがありませんが,一部共有します.(そのうち原稿も公開されるので,そのときに改めていろいろ書ければと思います)

時系列に並んだ金融文書からの差分抽出タスクの提案

理由を表す手がかり表現に基づく非論理的な言語推論

自動運転のための大規模走行データセットを用いた深層学習による信号機認識

即時配送と計画配送の実応用にむけた配送最適化アルゴリズム開発

固有表現抽出によるニューステキスト内の企業名抽出

社内コンペティションによる閉店拠点の予測

その他では,以下のコンペの入賞者報告会も大変勉強になりました.

AIコンペティション「ブルーカーボン・ダイナミクスを可視化せよ!」成果報告会

自身は忙しくて,baselineのnotebookを公開してそのままになってしまいましたが,次があればきちんと取り組めたらと思っております.

感想

想定していたよりも多くの方に認知してもらえていたことがとても嬉しかったです.

対面の集まりなどには,性格上なかなか参加できておらず,自分から声をかけに行くのも苦手なので,声をかけていただいた方々には感謝しかありません.

期間中はかなり予定が入っており,ご飯とかはなかなか行くことができずに終わってしまった方も多かったので,近々東京で会を企画できたらと思います.

kaggle masterに到達していない自分が企画することで,少しでも参加のハードルを下げることができたらと思っております.

そのときには,たくさんの方の参加お待ちしております.

自身の発表に関しても,多くの方に来ていただきありがとうございました.

当初は誰も来ない時間が多かったらどうしようかと思っておりましたが,あっという間に時間が過ぎるほど途切れることなく来ていただけました.(そして見に行こうと思っていた後ろのセッションの発表が終わっていました(笑))

引き続き微力ではありますが,コミュニティに貢献していくことができたらと思っておりますので,今後ともどうぞよろしくお願いいたします!

静岡でまた皆様とお会いできるのを楽しみにしております!

ケロロ軍曹

RTX A6000の購入を決意した話

はじめに

データ分析コンペに参加し始めて3年目を終えようとしていますが,「実力不足」×「時間不足」×「分析環境不足」の3不足状態でのコンペソロ参加は負けが続いており,コンペ終盤ではLBの下の方で炭治郎メンタルになる日々を過ごしております.

何とかこの状況を打破したいと考え,この度GPUの購入を決意しました.

「実力不足」に関しては,参加し続けることで少しずつ成長していくしか道はなく,「時間不足」に関しても仕事が第一なので,消去法で「分析環境不足」を解消することにしました.

実際,実力も時間もない中で,週に使えるkaggleのGPU時間の制約,GPU notebookの実行時間の制約,使用できるメモリの制約(モデルサイズやバッチサイズなど)などを考慮してモデルを学習させるのに精一杯で,+αの工夫をする余裕がないことの方が多かったです.

もちろん,GPUなしで上位に食い込む解法があるコンペも存在しますが,やはり上位争いできるコンペが制限されることや,試せることが制限されてしまうのは大きなディスアドバンテージでした.

そこで,以前にも少し調べていたことを踏まえて2択まで候補を絞り,以下のようなツイートをしたところ,ありがたいことに多くのコメントやご意見をいただくことができました.(ありがとうございます!)

この記事は,いただいたコメントの紹介と,それらを踏まえてRTX A6000を自身が選んだ理由を残したものです.

GPUの購入を検討している人の参考になれば幸いです.

RTX A6000 1枚 vs RTX3090 2枚に対するコメントなど

いただいたリプライ or 引用リツイート(Special Thanks!)

鍵アカからのリプライ

予算的に余裕あるならA6000がいいと思います! 1GPUの方が便利なこと多いです!

関連するツイート

RTX A6000購入の決め手

いろいろな情報を踏まえた上で,RTX A6000の購入を決めました.

規模の大きいモデルを学習させることができる

近年,モデルのパラメータ数や扱いたいデータサイズは大きくなる一方なので,48GBは最大の魅力でした.

マシン構成がシンプル

初めてのGPU購入で複数GPUでの構築は,少しハードルを感じました.

並列で実験を回すためには,それ以外のパーツのグレードアップが必要ですし,排熱なども気をつかう必要があるなどがハードルを感じる理由となります.

また,電気代が高騰しているので,消費電力(夏場の冷房なども含む)が増えるのは地味に痛い出費だと思いました.

今後,本当に必要になったら2枚目のRTX A6000を購入して,2枚構成にしたいと思います.

おわりに

想定したよりも,非常に多くの方の考えを共有いただき,感謝しております.

今回は,自身のお財布事情で,RTX A6000 一枚購入ぐらいの価格帯で検討しているというツイートに対して,それより上側の情報共有が多かったですが,実際にこれからコンペを始めるとかであれば,kaggleで無料で使える範囲から試しつつ, RTX3090,A4000,RTX4090あたりを1枚購入とかからでも十分かと思っております.

そのあたりに関しては,記事とかいろいろあるかと思いますので,そちらを参照してもらえたら幸いです. (例えば,前にこういった記事を紹介していただいたことがあります.A4000という選択 - Qiita

実際に購入してどうだったかについても,そのうち共有できたらと思っております.(ものすごく快適に分析回せてますという報告になるか,事故報告になるかわかりませんが)

良いGPU手に入れたのに上位争いできないようでは,ネタにされかねないので,引き続き頑張りたいと思います.

また,分析環境が整って,実力ももう少し着いたら,序盤からチーム組んでわいわいコンペしたいなと思っております.(経験不足&学習環境最弱過ぎて,ソロ参加ばかりしてきたので)

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

今後とも,どうぞよろしくお願いいたします.

【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の記事のリンクを貼り付けたスレッドにしていただければ,返信できるかと思います.

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

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