コード構成とデバッグ

能見氏(以下、能見):次はコード構成とデバッグの話です。Time-seriesコンペに関して、Kaggle環境でコードを書き切るのは、コード量が多くなるのでけっこうつらくなりがちです。そのため、手元で書いてGitで管理することをおすすめします。ただ、Time-seriesコンペでは信頼性の高い、わりと複雑なコードを書かなければいけないので、デバッグやテストの管理がしやすいように書きたいというのもあるかなと思っています。

自分がやりやすい方法で書くのが一番かと思いますが、私の場合は、ロジックを全部スクリプトに移動させて、Notebookはそれを呼び出すだけというかたちにする方向に落ち着きました。

分割したスクリプトをどうやってKaggle環境で実行するかは、(スライドを示して)下のリンク(※)をご覧ください。

https://www.m3tech.blog/entry/2021/01/13/180000
https://zenn.dev/hattan0523/articles/c55dfd51bb81e5

また、Time-seriesコンペでは専用のモジュールが必要なので、手元で書いても動かせないという大きな問題がありますが、これを解決するAPI Emulatorという良い方法があります。

Time-series APIと同じインターフェイスで、train.csvのデータをちょっとずつ返すコードを手元で用意して、手元ではそれを使って実行する、要はモックですね。これは、サブを消費せずに手元でデバッグできるのですごく有用で、個人的には必ず使ったほうがよいと言い切れるぐらい良いものだと思っています。

もし手元で実行する場合は、環境を揃えることを忘れずにやっておきましょう。

あと、ベンチマークも大事です。高速化は、もちろん個々のテクニックも大事ですが、再現性のある時間測定環境を作ることが、私は一番大事だと思っています。

Notebookから実装の本体をスクリプトに移動しておくと、本番のNotebookは汚さずに本番で呼び出すコードをベンチマークするようなスクリプトを書けるので、そういう方法を採ると個人的にはけっこうよかったです。

エラーハンドリングのためにすること

最後にエラーハンドリングの話です。

これは参加したことのある方はよくわかると思いますが、このコンペでサブ時にエラーが出ても、エラー要因はほとんどわかりません。Kaggle環境上で問題を切り分けるのはかなりつらいので、そもそもエラーが出ないように書きたいという動機があります。

それからコードコンペには2-stage制という恐ろしい制度があって、締切後にホストがデータを差し替えて再実行した時にエラーが出ると失格になってしまいます。Time-seriesコンペはデータの性質との兼ね合いもあって、過去4回のうち3回は2-stage制になっています。このルールではエラーが起きると取り返しがつきません。極端に言うと、どんなデータに差し替えられても絶対に死なないコードを書く必要があります。

では、Time-seriesコンペで最低限何をしたらよいのかというと、(スライドを示して)推論のforループの階層で例外をとにかく全部キャッチしてしまいましょう。絶対このpredictだけは呼んでしまう、といった書き方にします。こうすると、基本的に自分が書いた中のコードでどんな事故が起きても、ダメージが推論1回分に抑えられるので、やるかやらないかでいえば、絶対やったほうがよい処理です。

ただ、コンペ初期は逆に問題の発見が遅れるという問題もあります。個人的にはコンペ初期はやらずに、終盤に忘れずに入れるというのを心懸けておくとよいかなと思っています。

それ以外にも例外のキャッチはなるべく細かく入れておくと、当然被害の範囲が小さく抑えられます。

あと、さっき話したAPI Emulatorの応用で、エミュレータが返すデータをわざと壊してみてもよいでしょう。わざと一部のカラムを欠損させたデータにして、自分のコードがきちんと動くか確認するテストをするのも個人的にはおすすめします。

例外処理のコード自体にバグがあって、例外をキャッチする先でさらに例外が出てしまうような予想外の問題は得てして起きるので、そういう問題をあぶり出すのによいかなと思います。

制限時間オーバーは例外をいくらキャッチしてもどうしようもないので、この見積もりもきちんとやっておきましょう。実際にサブミットした時間を計ったり、エミュレータで実測したりと、いろいろな方法で推定して突き合わせてみるとよいんじゃないかなと思います。

あとは、これはテクニックというよりは心構えの話なのですが、私が大事にしたほうがよいと思っている点を1点共有しておきます。それは「暗黙の仮定」を意識しましょうという点です。

2-stage制の場合、Kaggle側から2nd stageでどういうデータが来るかはあまり詳しい仕様を与えられません。「それってどうなの?」みたいに思わなくもないですが、そういう条件の下でロバストなコードを書こうとした時に、「自分のコードはホストが言っていないことを暗黙に仮定しているんじゃないか?」という自問自答してみることは、大事なのではないかと思います。

推論Notebookのテンプレート紹介

ここまでいろいろ言ってしまいましたが、Time-seriesコンペで私はこういうかたちで書いているというテンプレートを紹介します。基本的に見たままなのであんまり解説するところもないのですが、この一番上で公式のモジュールとエミュレータを環境によって呼び分けて、どちらでも動くようにしています。

(スライドを示して)1個、ちょっと見ただけではわかりづらいところはこのへんですね。推論に渡すデータフレームに予測値を代入しますが、この代入を1ヶ所にまとめています。

例えばアンサンブルする時は、このコードの中で何回も代入して差し込んで、最後にこのモデル数で割るみたいなことをつい書きたくなりますが、そうすると途中で例外が発生した時に、割る前の半端な値が代入された状態でpredictしちゃいます。

こういう「例外が起きた時に整合性のある状態を保てるか?」みたいな考え方は「例外安全性」と呼ばれています。C++などを書いたことがある人にはよく知られていると思いますが、あまりKaggleでは聞いたことがないので、ちょっとここでお話ししました。このへんを考えておけるようになると、一段階信頼性の高いコードが書けるようになるんじゃないかなと思っています。

本日のまとめ

今日はTime-seriesコンペで大変なポイントと、それに対する4つの処方箋を解説してきました。

最後に、今日の話を聞いて「やはりTime-seriesコンペって面倒くさいな」と思った方もけっこういるのではないかと思います。しかし、個人的には現状メダルが取りやすい仕様のコンペだと思いますし、大変さを乗り切ると、実装力は間違いなく付くと思っています。

(スライドを示して)今日は、このへんのバッチとストリームの両対応が大変とか、状態管理がどうとか、今日はお話ししませんでしたが、アーティファクトの依存関係が大変とか、Time-seriesコンペでだいたい苦しむようなポイントは、現実のデータ分析基盤でもリアルな課題になっているのではないでしょうか。

今後もTime-seriesコンペはたぶん継続的に出てくると思うので、みなさんもぜひ参加してほしいなと思います。ということで、今日の発表を終わりたいと思います。ありがとうございました。

質疑応答

司会者:ありがとうございました。いや、メチャクチャ知見が詰まっていてすごいなと感じました。

ちなみに、Riiidでもtraining dataがすごい量だったと思いますが、Train用の特徴量を作ったりするのも、ストリーム処理をしたのでしょうか?

能見:そうですね。Riiidコンペのtrain dataは2億ぐらいでしたっけ?1億でしたっけ?まぁまぁありましたよね。でもGCPのPreemptible Instanceで、64コアぐらいのやつでフワッと回したら10〜15分ぐらいで計算が終わりました。即座ではありませんが、私のKaggleのやり方的には、ぜんぜん問題がないぐらいのスピード感でした。

司会者:なるほど。すばらしい。ちなみに、MLBコンペの話で、例えば2日目の推論をしようとした時に、1日目の特徴量は使いたい場合、NFLでは使えたと思うのですが、特徴量を作る・作らないみたいな2パターンがあると思っていればよいんでしょうか?

能見:特徴量というか、目的変数ですかね。

司会者:あ、そうですね。目的変数か。

能見:はい。Riiidでは1ステップ前の目的変数を取得できたので、それでラグ特徴量が作れたのですが、MLBの場合は、それが取得できないかたちになっていたので、テスト期間中に対するターゲットの値がわかりませんでした。

Time-seriesコンペも毎回微妙に仕様が変わっていたり、テストデータの期間の開き方もたぶん毎回微妙に違ったりすると思うので、そのへんはよく注意したほうがよいと思います。今回のMLBでは、あまり最初にホストからそのへんの情報がはっきり与えられていなくて、ほかの日本人Kagglerの方がよい感じに質問してくれて答えが出てきたところがあったので、非常に助かりました。わからなかったら質問してみるのも大事だと思います。

司会者:なるほど。ありがとうございました。

能見:ありがとうございました。