俵言

しがない社会人が書く、勉強とかのこと。最近は機械学習や kaggle 関連がメイン。

DSB2019 のラスト5サブを全て Error で溶かした kaggler がいるらしい

はい、僕です。

この記事は最近(2020-01-22 23:59 UTC)までkaggle で行われていた Data Science Bowl 2019 の反省記事的なやつです.

www.kaggle.com

最終日だけ冒頭の error を(しかも5サブ分全て)起こしてしまい, 非常に残念な気持ちでコンペを終えました(幸いにして一応メダルは獲れた).

本当は気持ちのこもった振り返り記事を書こうと思ってたのですが(短期間とは言えしっかり kaggle に取り組めたのが久々で, しんどいけどめちゃくちゃ楽しかったこととか, pandas 芸で学んだこととか, ... etc.), この error がどの処理で起きたのかだけは確かめたかったので先行して書くことにしました.

メモリエラーかと思ったらメモリエラーでは無かったっぽい?(ちょっとここ不明瞭なのですが...) とりあえず特徴量増やしたら死んだという感じでした.

因みに既に振り返り記事を書かれている方が何人かいらっしゃいます(今後も増えそう).

naotaka1128.hatenadiary.jp

socinuit.hatenablog.com

この記事では解法とかの話ではなくerror に絞った話をするので, こういった記事の方が雰囲気がつかめるかもしれないです.

submission error に苦しんだ人の目に止まれば良いな...


目次

データの簡単な説明

今回の error は特徴量作成の過程で起こっていたので, まずどういうデータだったのか(そしてどう特徴を作成したのか)を軽く述べます.

Data Sience Bowl 2019 のお題は 幼児用(3-5歳) STEM 教育アプリ "PBS KIDS Measure Up! app"(こちらで Web 版が遊べます) での活動履歴から, 各スキルに関する評価 (例: 大小関係を理解できているか?)の評価値を予測するというものでした.

詳細は省きますが, データとしてはアプリにおける細かい活動履歴が匿名化された状態で渡されています. 具体的には, 最小の単位は event (例: あるゲームをスタートした, どこどこの座標をクリックした, など)であり, これが渡されるCSV の 1行に相当します.
そして, 意味的なまとまりのある一連の event が集まったものが session (Game をプレイする, Video をみる, など) と呼ばれています.

f:id:Tawara:20200125054836p:plain
session と event の関係

各ユーザに関していくつかの session の情報が渡されていますが, その中にはスキル評価に使われる Assessment と呼ばれる session が含まれており, これらの評価値(0, 1, 2, 3) が別途 csv で渡されています(因みに, ユーザが submit を行わなかった session についてはラベルがついてません).

f:id:Tawara:20200125055337p:plain
各ユーザに関する Session

この評価値は Assessment Session の中身から復元可能な為, test data の scoring に用いる session については Assessment の最初の event( Assessment の種類やそのsession の開始時刻はわかる)だけが渡されるという形です.
因みに scoring に用いない session については, 運営から明示的に評価値の csv が渡されていないもののラベルが復元可能であるため training に用いることができました(僕も使ってました).

データを見てわかるように今回のデータは1行1行がラベル付けされた形ではなく, ログデータから特徴を自分で作成する必要があります. このため系列データに対する特徴量作成の勉強という意味ではとても良いコンペで, 僕自身参加することで得たものはかなり大きかったです.
ただそこで楽しく pandas 芸してたらsubmission error になっちゃったんですけどね

特徴量の作り方の方針

こっからが本題なんですが, 今回のコンペでは 各 Assessment に対してそれより過去のデータを用いて特徴を作成する必要があります.

f:id:Tawara:20200125060824p:plain
Target Assessment Session に対して使える情報

で, こういう場合は過去の session の情報を集約して特徴を作るのが基本ですが, そもそも session 自体も複数の event から成っているので, 着目する session ごとに計算を全部やり直すのは時間がめっちゃかかります.

そこで, (多分多くの人がそうだったと思うのですが) 一旦 session ごとに event の情報を集約してしまった中間生成物 (僕は session_info と呼んでました) を作成し(元々の train , test はこの時点で削除), この session ごとの情報を使って各 assessment に対する特徴を作ってました.

各 target assessment に対する特徴量を作成する場合も, event -> session と同様の流れで session_info -> target assessment

の集約が行われます. (ここで groupby や pivot_table などの pandas 芸が大活躍します.) ただ注意しないといけないのは同じユーザでも target となる assessment session ごとに使える情報の範囲が異なるという点です.

f:id:Tawara:20200125064326p:plain
target session ごとの集約の範囲

というか, そもそも 集約を行う際に集約を行うための id(今回のコンペではユーザの識別子は installation_id と呼ばれるもの) が同じ(しかしながら集約の範囲は異なる)が混在していると色々めんどくさいことになります.

なので, 僕の場合は Assessment に対する特徴づくりを行う場合に同じユーザの Assessment は一つだけの状態にして特徴づくりをする(のを繰り返す)とい形をとりました. もうちょい詳しく言うと, 「各ユーザの最終の assessment から n 個目の assessment」の特徴を一気に作るという方式です.

f:id:Tawara:20200125065805p:plain
特徴量作成の様子

この様子みるとわかるんですけど Assessment の数ってユーザごとだとかなり少なくて, 50 before とかになってくると数人しかいない状態です. そういう意味ではこのやり方は無駄があります. また, 同じユーザについては何度も集約の範囲が被るのでそこも無駄な計算が行われてます. 他の参加者がどう特徴量生成してたかめっちゃ気になりますねえ...

最終日辺りに「これって accum 系使えば 同一の installation_id が含まれてても良い感じに出来るかも」って思い至ったんですがまだ試していないです. そのうち 集約芸の記事書きたい.

まとめると, 僕の NoteBook の流れは上記の前処理・特徴作成を含めた

  1. 準備: import, 関数定義, data の読み込み
  2. 前処理1: 各 event に対して情報を付加(text data の parse など)するなど
  3. 前処理2: event を集約して session info を作成 (元の train, test は削除)
  4. session info から 各 Assessment に対する特徴量を作成
  5. 学習・評価・予測
  6. 閾値の探索・submit

※5 ~ 6 はこの記事では触れません.

という感じです. そして error が起きたときは多分 3 が怪しいなと思って調査してました.

何の処理で落ちていたか?

ここまでで集約芸の話をしていて, 上でも 3(event -> session の集約) が怪しい と言っていたのですが, 細かく submission を繰り返してみるとどうやら 2(各 event の加工) の時点で error が起きていたことが判明しました.

何かというと pandas.Series.str に対する 正規表現での処理です.
各 event には event_data というログの json file を文字列としてぶち込んでいるカラムがあり, こいつを parse して作る特徴を最終日に入れたのですが見事に死にました.

  • "misses": の直後の数字を抽出
train["game_misses_count"] = train.event_data.str.extract('"misses":([0-9]+)').fillna(0).astype(int)
test["game_misses_count"] = test.event_data.str.extract('"misses":([0-9]+)').fillna(0).astype(int)
  • "game level": の直後の数字を抽出
train["game_level"] = train.event_data.str.extract('"level":([0-9]+)').fillna(0).astype(int)
test["game_level"] = test.event_data.str.extract('"level":([0-9]+)').fillna(0).astype(int)
  • round の直後の数字を抽出
train["game_round"] = train.event_data.str.extract('"round":([0-9]+)').fillna(0).astype(int)
test["game_round"] = test.event_data.str.extract('"round":([0-9]+)').fillna(0).astype(int)

因みに pandas.Series.str.contains で存在判定するくらいなら多分大丈夫だったったぽくて(この処理も別の特徴のために使っている), 正規表現で抽出するからダメだったっぽいです.
「一気に抽出できて楽でええやん!」と思って使ったら何ということでしょうと言う感じ.

因みに, この処理で出た Submission Error は Notebook Exceeded Allowed Compute なのですが, こいつがメモリを超えたことを指すのか CPU負荷が限界に至ったのかはよくわかってないです.
メモリのエラーは Memory Limit Exceeded 的なのが出るのかと思っていたのですが, late sub での検証(test data を 100倍くらいに水増しする) を行っても同じメッセージでした.

さて, 上記の正規表現での抽出は一気にやらずに1/10ずつくらいで適用すれば解決したのですが, このあと結局 3(event -> session の集約) でも同じ error が出ました.
どうやら今度は普通に特徴量の数が増えたから groupby するときに死んだという話っぽいのですが, (最終日より前の成功していたsub に比べて) そんなに増えていないのになあという感想です. まあ多分元々からギリギリの橋を渡っていたのでしょう.

そんなこんなで, 各処理を分割してちょっとずつやることでめでたく late sub が成功しました.

ちょっと解せないところ

おおよそ 8倍とされていた公式の言葉に従って

test_list = [test.copy()]
for i in range(7):
    test_list.append(test.copy())
    test_list[-1].game_session = test_list[-1].game_session + "_cp{}".format(i + 1)
    test_list[-1].installation_id = test_list[-1].installation_id + "_cp{}".format(i + 1)
test = pd.concat(test_list, axis=0, ignore_index=True)
del test_list
gc.collect()

みたいに水増ししても何でか普通に実行できました(commit も出来た).

また, もうちょい増やした場合,

も実行できたのですが, こちらの場合は commit には失敗しました(後から試したところ判明).

今回は groupby 等での集約が絡んでいるため, 単純に増やすだけでは同じ状況にはならないのかあという感想です. 次の時は少し多めにして検証して submission に移るように気を付けようと思います.

おわりに

というわけで submission error のお話でした.

に引き続きまたも code competition で失敗をしてしまいまいたが, 集約系が絡んでくると test data の増量に対する計算量とかメモリ消費量が読みにくくなるので気を付けなきゃな―という感想です. これを読んでいる方も pandas 芸を行う場合は(特に code competition の場合は) お気を付け下さい.

さて, この記事は何故か atmaCup 当日の早朝に目覚めてしまい, その時間を有効利用して書いています(なぜお前は atmaCup の準備をしていないのか...)

atma.connpass.com

kaggle days tokyo => DSB2019 と経験したことで前よりはテーブルコンペで出来ることが増えているので(というかテーブルコンペですよね?テーブルコンペと言ってくれ...!) 楽しみたいと思います!

参加される皆さん、対戦よろしくお願いします!

おまけ:今回のオチ

やっと late submission 出来るし, 出せてたらどうだったのか確認してみるか~(わくわく)

               .
               .
               .
               .
               .

f:id:Tawara:20200125081112p:plain
Final Day Submission をもう一度

...ってメダル圏のサブ(ほぼ)ないやんけ!

神様が「それ以上は行ってはいけない...」って言ってくれてたのかもしれないですね。
最後の 2 sub は error 回避のために投げたやつだったので置いとくとして, 最初3sub は一応 cv が上がったものの筈だったのですが... まあちゃんと cv 作れてなかったんやなって(truncated cv とかコンペ終了後に知った顔).

というわけで今回も半分運でメダルとってしまった形になってしまいました. 次こそはきっちり実力でテーブルコンペのメダルを獲りたいなって思います.

まあ運が良いのは悪いことでは無いですけどね.