Kaggle参戦記：参戦5日目で銀メダル圏内へ（4090人中82位　上位2%）

　いや～、今日は気持ちが良い。[生成AIと人間の文章を区別するコンペ](https://www.kaggle.com/competitions/llm-detect-ai-generated-text/overview)で、これまで0.961だったスコアを0.962に微増することができました。たった0.001ポイントですがその差は大きい。おかげで順位は4090人中82位。銀メダル圏内に入りました。[参戦](https://qiita.com/rho-guy/items/e6bc4ce56a4d85c01655)5日目にしての快挙。そして次の金メダル圏内のスコアは0.968。これまでよりずっとハードルが高い戦いが続きます。締め切りまで残り12日。最後の追い込みも油断せずに攻めていきます。

# 新たな壁の登場：ノートブックのセッション切れ
　データ量や特徴量ベクトルのサイズなど、さまざまなパラメーターを調整しながら、メモリのオーバーフローぎりぎりのところで実行させる状態が続いています。Submit後のRunでメモリエラーが出てスコアがつかないのは悲しいので、手元のノートブックで、想定されるテストデータと同程度の規模（約1万件）のデータを処理させるようにしたのですが、ここで、新たな壁が登場しました。

 それが「ノートブックのセッション切れの壁」です。調べてみると、Kaggleのノートブックは[最後の操作から1時間以上触っていないと、セッションが切れてリロードが必要になる](https://www.codexa.net/kaggle-notebook-for-beginners/)そうです。1万件のテストデータにかかる処理時間は3時間くらいなのですが、実行中のノートブックをバックグラウンドにして他のことをしていると、セッションが切れて実行が止まってしまいます。

 今日は、この制約をどのようにして乗り越えたのか（もっと正確には「どのようにやり過ごしているのか？」）について、詳しくお話します。

　参考にさせていただいた[元のソースコード](https://www.kaggle.com/code/erikfolkesson/detailed-explanation-custom-tokenizer)では、ノートブックの環境と、Submit後の環境を区別して、下記のように実行していました。testデータのサイズが5件未満（＝ダミーデータ）の場合は、分類器を通さずに終了します。一方、Submit後のRunでは、5件以上のtestデータ（＝実データ）が格納されるので、その場合は分類器を動かしてsubmit.csvファイルを出力しています。
 
```mermaid
graph LR
  Start([Start])-->B{dummy test?}
  B-->|True = Notebook| End
  B-->|False = Submit| CLF[分類器]
  CLF-->RES[分類結果をsubmit.csvに保存]
  RES-->End([End])
```

　それに対して、私が[前回の記事](https://qiita.com/rho-guy/items/8668827513f226920310)で行った改造は下記の通りです。testデータがダミーだった場合、手持ちのtrainデータを分割して、testデータを作って分類器を動かし、メモリオーバーエラーが出ないかをノートブック上で確認しようとしていました。しかし、これでは上述の通り「セッション切れ」の壁にぶつかります。
```mermaid
graph LR
  Start([Start])-->B{dummy test?}
  B-->|True = Notebook| SP[trainデータ分割]
  B-->|False = Submit| CLF[分類器]
  SP-->CLF
  CLF-->RES[結果保存]
  RES-->End([End])
```

 　ここで、実行環境についてよくよく整理してみると、「ノートブックでの実行」～「Submit後のRun」の間に「Save&Run」で実行する環境もあります。調べてみると、[KAGGLE_KERNEL_RUN_TYPE](https://www.kaggle.com/code/jamesmcguigan/kaggle-environment-variables-os-environ)という環境変数を通じて、どの環境で実行されているかを取得することができることも分かりました。

　そこで最終的に、下記のような構成で、「①プログラムの動作確認」はノートブック上で、「②メモリオーバーフローの確認」はSave&Runの環境で、別々に行うことにしました。

```mermaid
graph LR
  Start([Start])-->ENV{KAGGLE環境変数}
  ENV-->|Interactive| SP1[簡易テスト]
  SP1--> CLF[分類器]
  ENV-->|Batch| B{dummy test?}
  B-->|True = Save&Run| SP2[負荷テスト]
  B-->|False = Submit| CLF
  SP2-->CLF
  CLF-->RES[結果保存]
  RES-->End([End])
```
  
  まず、環境変数KAGGLE_KERNEL_RUN_TYPEの値を調べると、「Interactive」か「Batch」かの値が返ってきます。「Interactive」の場合は、ブラウザのノートブック上で実行していることになるので、trainデータを分割した100件くらいの少量のテストデータを通して、「処理が全体を通じてエラーなく通るか」だけの「①プログラムの動作確認」の簡易テストをします。

  ノートブック上での簡易テストが正常終了すると、そのコードを「Save&Run」させ、Kaggleのコンテナ内で実行させます。これは、バックグラウンドで実行してくれるので、処理時間が数時間かかっても問題ありません。コンテナ内で実行する際に、testデータのサイズを見て、ダミーだとわかったらtrainデータを分割して、本番と同程度のサイズのtestデータに置き換えて、負荷テストを行います。これによって、「②メモリオーバーフロー」の有無を確認できます。（私は今のところ問題にはなっていませんが、実行時間が制限時間内かどうかの確認もこの段階でできますね）

  「Save&Run」でも問題が出なかったノートブックは、Submitで提出します。すでに、動作時間もメモリエラーも問題ないことを確認済みなので、「途中で止まったらどうしよう？」などと考えず、気を楽にして結果を待つだけです。精神衛生的にもよいですね。
  
ロジックにかかわる部分は、コンペ終了後まで開示できませんが、上記の動作テスト用のソースコードを共有しておきます。ご参考になれば。

ノートブックの最初の方で実行しておくセル
~~~.py
# Kaggle実行環境の判別プログラム

# testデータを読み込む
# Submit前（ノートブック上 or Save&Run時）なら5件未満のダミーデータ
# Submit後なら、5件以上の実データがtestに読み込まれる
test = pd.read_csv('/kaggle/input/llm-detect-ai-generated-text/test_essays.csv')

# 環境変数をチェック　⇒ (1)Notebook, (2)Save&Run, (3)Submitを判定
if os.environ.get('KAGGLE_KERNEL_RUN_TYPE','') == 'Interactive':
    print("Running on (1)Notebook...")
    param["run"]["RUN_ENV"] = "Notebook"
elif os.environ.get('KAGGLE_KERNEL_RUN_TYPE','') == 'Batch':
    if len(test) < 5:
        # testデータのサイズが5件未満ならSave&Run環境
        print("Running on (2)Save&Run")
        param["run"]["RUN_ENV"] = "SaveRun"
    else:
        # testデータのサイズが5件以上ならSubmit後のRun
        print("Running on (3)Submit")
        param["run"]["RUN_ENV"] = "Submit"
~~~

testデータを読み込んだ後に実行するセル
~~~.py
if param["run"]["RUN_ENV"] == "Notebook":
    # Notebook上では小さいサイズのtrain, testデータを使う（動作確認のみの簡易テスト）
    train, test = train_test_split(aggregated_train, train_size=100, test_size=100)
elif param["run"]["RUN_ENV"] == "SaveRun":
    # Save&Run環境では、大きいサイズのtarin, testデータを使う（メモリオーバーフローを確認する負荷テスト）
    train, test = train_test_split(aggregated_train, test_size=10000)
else:
    # Submit環境では、手持ちの全データを学習データとして使う
    train = aggregated_train
~~~