1.まえがき

今年の8月中旬ぐらいからKaggleという運営会社が提供するデータ分析コンペに参加し始めたのですが、10月6日に終了したOSIC Pulmonary Fibrosis Progressionというコンペ(以下OSICコンペ)1にて2097チーム中2位でソロゴールド+賞金を獲得することができました。

もちろん、運が味方してくれた結果でもあり、データ分析コンペの性質上こうしたビギナーズラックは一定割合で起こりうるものかと思います。ただ、自分にとっては大変喜ばしい出来事だったので、一連の流れを書き残しておこうと思います。本記事の趣旨としてはあくまで次のようなものになります。

  • 初心者が上位入賞することができた一例のシェア

  • コンペ中の視点や考えを中心とした体験談

  • 将来の自分の振り返り用の備忘録

というわけで、技術的に高度な話や具体的な勉強法、詳細な解法といったものを期待される方には物足りない内容かと思います。そもそも、今回のコンペは結果的に特別高度なテクニックが必要とされるものではなく、私の解法も次のようにコンペに参加していた人からすれば特段説明もいらないようなシンプルな内容でした。

  • EfficientNetB3(CTスキャンデータ) + Quantile Regression(メタデータ)

  • 患者をクラスタリングし、Group K-FOLDを適用することで、診察結果単位ではなく患者単位で訓練データの分割を行う

  • Percentを初期値に固定する

もちろん、複数の観点から検証を進め、結果として本解法をFinal Submissionとして選んだという経緯はあります。今回はその過程に重きを置き、私と同じように最近Kaggleを始め、メダルの獲得を目指している人の参考になればと思います。

2.OSICコンペの概要

データについて

今回のコンペは、肺線維症という肺胞の壁が硬くなり、酸素の取り込みに支障をきたす難病における患者さんの努力肺活量(FVC)を予測するというものでした。各患者の診察結果(メタデータ+CTスキャン)が次のような形式で与えられ、訓練データには176人、テストデータには約200人の患者の診察結果が含まれていました。

メタデータ

各患者のFVC測定値とメタデータが次のような項目で与えられました。

カラム名 意味
Patient 患者さんのID
Weeks CTスキャンデータ撮影日を基準とした時間経過
FVC 努力肺活量(今回の予測対象)
Percent FVCについて、近い属性を持つ人の典型値に対する比率
Age 年齢
Sex 性別
SmokingStatus 喫煙状況を「Ex-smoker」「Never smoked」「Currently smokes」の3種に分類

これらのメタデータについて、訓練データには患者さん毎に複数回の診察結果が与えられ、テストデータについては初回の診察結果のみが与えられるというものでした。

CTスキャンデータ

患者さんごとに、Weeks=0におけるCTスキャンデータが与えられていました。訓練データ、テストデータともにWeeks=0における1回分のデータのみが与えられています。

こちらはCTスキャンデータの一例で、それぞれ異なる患者・スライス位置のものになりますが、画像サイズや形状が大きく異なることがわかりますね。

テストデータと評価

これらのデータをもとに、テストデータに含まれる患者のFVCを任意のWeeksについて予測し、最終的には各患者の最後の3回分の診察結果と予測値をもとに評価が行われました。なお、テストデータのうち15%がPublic Scoreの算出に使われ、残りの85%がPrivate Scoreの算出に用いられました。

コンペの特徴

以上を踏まえた上で、今回のコンペにおける特徴をいくつかまとめてみます。

  • 全体的にデータ数が少ない

  • 特にPublic Scoreの算出に使われるテストデータは極めて少ない

  • FVCと完全に相関関係にあるPercentという特徴量が与えられている

こうした事情から次のような状況が発生していました。

  • CV ScoreとPublic Scoreが相関しない

  • Public Scoreに過学習したNotebookが多くの人にfork&submitされ、リーダーボードが機能しない

  • 上記のNotebookは「Percent」をそのまま使用しており、これによるリークに気付いていなかった人が多く存在した(推測)

  • CTスキャンデータの活用が難しく、ほぼメタデータによる予測合戦だった

このような状況の結果、Public LBとPrivate LBとで非常に大きく順位が変動する結果(俗に言うShake up)となり、Private LBの上位陣はほぼ軒並み3桁以上順位をあげての入賞となりました。

こうした中、次のような解法をFinal Submissionとして選べていた人は、他に余計なことをしない限り、少なくともPrivate LBのメダル獲得圏内には入れたのではないかと思います。

  • Percentを特徴量から削除する、あるいは最初の診察結果のみ用いる

  • 交差検証におけるデータ分割の最小単位を診察結果単位ではなく患者単位で行う

上記の手法はある程度の人が一度は試していたのではないかと思いますが、これをFinal Submissionとして選ぶというのも簡単なようで案外難しいところだったのかと思われます。それでは、実際にコンペ参加からコンペ終了までの取り組みを説明していきたいと思います。

3.コンペ参戦~序盤

コンペ選び

まず私が初めて参加した「Cornell Birdcall Identification」というコンペ(以下鳥コンペ)2が終わる9月中旬頃、そろそろ次のコンペに参加しようといくつかコンペの概要を見ていました。鳥コンペは音声データを扱うコンペだったので、次はテーブルコンペか画像コンペが良いなと思っていたところ、画像データを扱える肺コンペがありましたので参加いたしました。参加者もそこそこ多く、残り期間が約3週間とコンペ終盤に差し掛かっていたため、DiscussionとNotebooksからたくさんインプットできそうだったのも理由の一つです。変に過疎化していたり、残り期間が長いと、アイデアが尽きたり、中だるみしてしまう恐れがあるため、こうした観点でコンペを選ぶのもありかと思います。

ちなみに鳥コンペの方は最終順位が159位/1390teamsとメダルには及びませんでしたが、ここでKaggleの仕様や用語に慣れることができたのはかなり大きかったと思います。

計算資源

計算資源についても軽く触れておきます。現時点で持っている計算資源は次の通りです。

メイン:Kaggle Notebooks
予備:Google Colab3、ローカルPC(GPU:GTX1060)

今回は「メイン」であるKaggle Notebooksのみを使用しました。予備環境については鳥コンペの際に整えたものですが、正直どちらも有効活用できていません。予備環境は、Kaggle NotebooksのGPU quotaが尽きた時用ではあるのですが、Colabについてはストレージ周りの関係でデータサイズが大きいコンペでの運用が厳しいです。また、ローカルPCについてはWSL2+dockerで運用していますが、WSL2上でのGPU利用時の不具合が多く、かつGPUの専用メモリも6GBと少ないため、少ないバッチサイズやデータサイズで簡単な実験を行うぐらいしかできません。逆にデータサイズが少ないコンペにおいてはKaggle Notebooksだけで事足りるため、結果的に出番がないといった状況です。

しかしながら、Kaggle Notebooksだけでコンペに参加するのもデメリットばかりではなく、

  • 計算資源が限られているため、一つ一つの手法に対する吟味が慎重になり、細かなミスに気付きやすくなる

  • 計算時間の削減にハングリーになれる

といったメリットもあります。もちろん、これらは一定のレベルの人にとっては当たり前のように徹底しており、クラウド課金の場合にも同様のことが言えたりします。しかしながら、KaggleやMLを始めたばかりの人にとっては、ある程度限られた計算資源の中でできることを探していくことで上記のような癖を付けるのも大切なのではないかと感じました。

初Submitまで

とりあえずルールを読み、概要を掴んだ上でデータの確認を行いました。前述の通りCTスキャンデータ(画像)と患者のメタデータ(テーブル)の2種類が与えられており、本来であれば画像コンペという分類が妥当でした。しかし、画像を扱う割にはデータ数が少なく、なによりPublicテストデータがかなり少ないのが印象的でした。

また、今回のコンペは「Code Competiton」という形式で行われ、学習はローカル環境で行っても良いが、推論(Submit)はKaggleのNotebooks上で行ってくださいというものでした。ここで最終的なスコアの計算に用いられるPrivateテストデータはSubmitしたコードが内部で再実行される時しかアクセスできません。コードの実行時間はコンペ毎に決められた制限時間内である必要があり、PublicとPrivate両方のテストデータに対して推論が行われるため、Notebooks上で制限時間をクリアしても、Submit後の再実行でタイムアウトエラーが出る(1sub分無駄になる)こともあります。これらの仕様は戦略を考える上で重要なのではないかと思います。

とりあえず1subをすることで嫌でも取り組むことになるだろうと、たくさんUpvoteされてるPublic Notebookをいくつか読み、一部をそのまま提出して、Public Scoreと実行時間の様子見を行いました。今回は制限時間に対して余裕がありましたが、それが戦略に大きく絡むことはありませんでした。

リーダーボードの状況把握

とりあえず、リーダーボード上ではfork&submitしたNotebookのスコア周辺で団子になっていることがわかりました。ただし、こちらのNotebookはPublic Scoreの動きを見ながらハイパーパラメータチューニングをしているということや、seedを変えたら大きくスコアが落ちることからも、Public Scoreに過学習していることは見て取れました。

後で思ったのですが、Upvoteの数が必ずしもNotebookの良し悪しを表しているわけではないように思います。特にPublic Scoreの高いNotebookはビギナーからの票を稼ぎやすく、Public Scoreに過学習しているだけのNotebookでも上位に来てしまうことが想定されます。Notebooks等を読み漁る際は「なぜPublic Scoreが高いのか?過学習しているだけじゃないか?」といった懐疑的な視点も持ち合わせておくことで、誤った解法を取り入れてしまうリスクを回避できるだけでなく、解法の中で良い要素だけを抽出することにもつながるのではないかと思います。

コンペの論点探しと方針決定

もう少し本質的なところで検証を進めていくべく、CTスキャン・メタデータ共にEDAを進めつつ、Discussionを少し読み漁ったりしました。ある程度調査していった結果、本コンペでは次のような特徴があることがわかりました。

 (1) 大半の人がCTスキャンデータよりもメタデータによる推論結果の方が良く苦戦している

 (2) CV ScoreとPublic Scoreが連動しない

(1)については自分の戦略を大きく決定づけるもので、「ほぼKaggle Notebooksしか計算資源がなく、残り期間は約3週間」という状況も加味した上で、CTスキャンデータの扱いは後回しにし、とにかくメタデータの扱いを極めて高い優先度で進めることにしました。(2)についてはとりあえず自分も同様の状況に遭遇してみないと何とも言えないといったと感じでした。

Notebookのカスタマイズ

とりあえず先程のNotebookの解法をベースに改良を加えていくことにしました。もっとも自分で一からbaselineを作成して、適宜他者のコードや意見を参考にしつつ組み込んでいくといった流れが理想(というよりかっこいい)ですが、残り時間と費用対効果を考え、モデルの構築以外の部分にリソースを割くことにしました。

学習の推移の可視化や出力等、足りない要素を補いながら、モデルの基本的な検証を行い、過学習気味なことを踏まえてエポック数を減らすなど、軽くハイパーパラメータを調整しました。また検証サイクルを回しやすくするために、メタデータの学習パートと推論パートを分離し、別々のNotebookに落とし込みました。

4.コンペ中盤

CV ScoreとPublic Scoreが相関しない問題

Notebookのカスタマイズ作業の中で、CV ScoreとPublic Scoreの変化の確認も行っていたのですが、前述の「(2)CV ScoreとPublic Scoreが連動しない」という問題に自分も直面しました。こうした中で闇雲にPublic Scoreを追いかけるのではなく、まずは信頼できるCV Scoreを獲得することが先だと考えました。そのためにはリークに対する慎重な吟味が必要だと考え、重点的に取り組んだわけですが、幸いにもこれが本コンペのキーでした。リークには様々なものが考えられますが、今回のモデルに関して言えば、「交差検証時のリーク」と「特徴量のリーク」の2つに問題がありました。

交差検証時のリーク対策と改良

まず、元のNotebookでは訓練データに対して直接K-FOLDを用いていたのですが、1人の患者の診察結果が複数のfoldsに存在するのはリークと考えられます。そのため、患者単位で訓練データを分割することにしました。また更に良好な交差検証法を探すべく、様々な手法で患者をクラスタリングし、Group K-FOLDを行い、比較することで検証の方を進めました。結果的には、最後の数回分のFVCの変化をもとにクラスタリングを行うというDiscussionにあがっていた方法4に落ち着いた上に、クラスタリング方法の違いによるCV Scoreの差はほとんど無かったのですが、この微妙な違いが細かな順位の差に繋がった可能性もあるため、損ではなかったかと思います。

また、CV Scoreの算出にはコンペの評価方法と同様に最後の3回分の診察結果のみを用いました。先程の手法の投稿者の方はバリデーションデータに含まれる患者さんの最後の3回分以外の診察結果を訓練データに回していたようですが、私はリークを懸念し行いませんでした。投稿者の方もPrivate10位でゴールドメダルを獲得されているので、どっちの手法が優れていたかはわかりませんが、後述の特徴量のリークに比べると、交差検証時のリークは致命的でなく、仮にそのままK-FOLDを行っていても、銀圏相当のPrivate Scoreが出ておりました。

交差検証法 CV Pulic Private
K-FOLD(診察結果単位) -6.0504 -6.9077 -6.8468
K-FOLD(患者単位) -6.5806 -6.8988 -6.8384
Group K-FOLD(患者単位) -6.5414 -6.8970 -6.8371

※アンサンブル前(メタデータによる推論のみ)の結果

特徴量のリーク対策

実はこちらのリークこそが最大の罠で、「Percent」という特徴量が「FVC」と完全に相関しており、そのまま用いてしまうと目的変数を説明変数に含んだまま学習を行っているのとほぼ同じことになるというものでした。テストデータについては最初の診察結果の値しか与えられていないため、「特徴量から完全に除外する」あるいは「最初の値のみ用いる」のいずれかが求められました。これに気付くこと自体はそこまで難しいことではないのですが、厄介なことに、「Percent」をそのまま採用した方がなぜかPublic Scoreが良いという現象に見舞われました。

Percent CV Pulic Private
そのまま -6.4797 -6.8414 -6.9340
初期値に固定 -6.5414 -6.8970 -6.8371
不使用 -6.5512 -6.8998 -6.8351

※アンサンブル前(メタデータによる推論のみ)の結果

最終順位の決定に用いるFinal Submissionは2つ選べるため、一つは「Percentを最初の値に固定し、リークを防いだ手法」、もう一つは「Percentを何らかの形で活用した手法」を提出するという方針を決めました。結果的に後者の取り組みは無駄ではありましたが、「最初のPercentの値からTypical FVCを逆算し、計算式から身長を見積もり特徴量に加える」「Percentの推移を予測し、特徴量に加える」といったことを試しながら、異なる方向性のSubmissionの準備を進めていきました。

ちなみに余談ですが、平日は仕事で出社し、帰りにジムでトレーニングをしたり、買い物に行ったりしているので、家でコードを書く時間はある程度で限られていました。ただ、外出時でもアイデアを生み出すことはできるので、出社や帰宅の移動時間、昼休憩、トレーニングのインターバルなどを駆使して思考を巡らせ、「家に帰ったらあとは実装するだけ状態」を作り出せるよう心がけました。バグでつまずいたり、思うような結果が出なかったり、そもそも良いアイデアが帰宅までに思い浮かばなかった時は萎えましたが、試行錯誤の回数を重ねることができました。

正規化のあれこれ

 特徴量をモデルにインプットする前に、どうスケール変換するかということも考える必要もありました。単純にMinMaxScalerがいいのか、StandardScalerがいいのか、といった統計的手法の選択の話もありますが、そもそもどのデータを母集団とするかという点について議論する余地があります。実際の業務等においてはテストデータの特徴量を学習に用いるのは良くないかもしれませんが、コンペにおいてはむしろテストデータの特徴量も含めて学習を行うことで、より高いスコアを獲得できる可能性もあります。更に、テストデータを含める場合に、PublicまでにするのかPrivateも含めて処理するのかという違いは戦略を大きく決定づけるものであり、Privateも用いる場合は、ローカルでの学習は不可能になります。結果的にSubmitの中で学習も行う必要が出てくるため、「コードの実行時間」と「Privateデータの利用」というトレードオフを考慮しながら、適切な手法を選択していく必要があります。

今回に関してはデータ数が少なく、より多くのデータをもとに統計的な処理を行いたいということと、過学習の観点からスタッキング等を行う予定もなく、実行時間に余裕があることから、学習・推論の両方を提出時に行い、Privateも含めた正規化を行うこととしました。また、手法については上記の手法等をCV Scoreをもとに比較しながら、最終的にMinMaxScalerを選びました。

CTスキャンデータの学習

以上でメタデータの学習についてはほぼ処理を終えました。CTスキャンデータは費用対効果からほとんど手をつけておりませんでしたが、前述の手法でGroup K-FOLDを用いて、交差検証を行い、EfficientNet-B3を使った再学習を行いました。オリジナルがB5だったのに対してB3を用いたのは、Kaggle Notebooks上ではオリジナルと同様のバッチサイズではメモリ不足にあってしまうことと、特に精度が落ちることもなかったためです。

この他に学習に用いるCTスキャンデータの選定手法を変えたり、Data Augmentationを試みたりもしましたが、いずれもうまく精度が出ず、3D-CNNの使用も少し検討しましたが、やはりデータ数が少なすぎるため断念しました。

ホスト側の意図としてはメタデータではなくCTスキャンデータをうまく扱って課題解決に貢献してもらいたいということだったのでしょうが、残念ながら参加者のほとんどがうまく精度を出せずに終わってしまい、例に漏れず私もメタデータ頼みとなってしまいました。コンペ主催者の意図通りの展開が繰り広げられるとも限らないのが、データ分析コンペ開催の難しいところなのでしょう。

5.コンペ終盤

アンサンブル

最後に結果のアンサンブルです。実はこの段階で、リーダーボードが機能していないため、全体における自分の立ち位置が全く読めず、かなりモチベを喪失しておりました。本来であれば、メタデータの方について複数モデルを作成し、ハイパーパラメータやBlendingにおける重みのチューニングを行うべきだったのですが、完全に放棄してしまいました。特にCTスキャンデータによる推論時に複数fold分のモデルを用いると異常に時間がかかってしまうバグが取り切れず、トータルでのCV Scoreの算出ができないという事態に見舞われ、完全にやる気を失ってしまいました。最後までやりきっていたら1位を取れていたとは言わないですし、むしろいらないことをして賞金を取り逃していた可能性もあったので、結果としてはそれで良かったのかもしれないですが、このツメの甘さが2位という順位に表われている気もします。もちろん自分にとっては大変良い結果であり、その順位にも満足してはいますが。アンサンブルの手法として、スタッキングは少ない訓練データに過学習してしまう可能性を危惧して行わず、CTスキャン及びメタデータによる学習結果を8:2の割合でブレンディングしました。

Final Submission

そして最後に自分が提出したコードのうちどの2つをFinal Submissionとして選ぶのかというところですが、1つ目は迷わずTrust CVで交差検証及び特徴量に関するリークのないモデルの中で最もCV Scoreの良いものを選びました。もう1つは「Percent」を予測し特徴量として加えた学習モデルでした。結果としては、前者が自身の提出コードの中でも最も良いPrivate Scoreであり、かつ2位を獲得することができ、後者については振るわずといった形に終わりました。

正直、Shake upがあるにしてもまさか自分が2位になるとは思わず、嬉しさと焦りや不安が混ざり合い、不思議な感覚になりました。「そういえば上位に入ったら賞金がもらえるんだっけ?」と思って調べたPrizeの額を見て余計に焦ったり、賞金受け取りまでにやるべきことがたくさんあったりと、むしろ不安な気持ちの方が大きかったかもしれません。

6.まとめ

今回に関しては、Trust CVが功を奏した、というよりTrust CVを行うために信頼できるCV Scoreの獲得を目指した結果、それが完成した頃には上位入賞のために十分な解法ができていた、といったところでしょうか。また、自分のCV Scoreとやってきたことを信じて、適切なSubmission選びができたことも良かったかと思います。メダル獲得にはリークの回避という比較的シンプルな手法だけで十分であったため、自身のSubmissionの中には金圏相当であったものの、惜しくも選ぶことができずといった方も一定数おられたのではないでしょうか。もし今回のコンペが、データ数が十分に多かったり、CTスキャンデータがハンドリングしやすい、などといった状況であれば、結果は大きく変わったかもしれません。様々な要因が複合した結果、幸いにも2位という順位をいただくことができましたが、まだまだインプットを増やしていく時期だと考えております。今回は運が良かっただけ、程度に受け止め、一発屋に終わらないよう精進の方続けていきたいと思います。

ちなみに賞金については、ソースコード・ドキュメントの提出やWinner's Callというホストとのウェブ会議の参加を経た後に授与されるのですが、これについてはまた別の記事で紹介したいと思います。