Kaggleの ROGII - Wellbore Geology Prediction に参加しました。
結果から書くと、
- 6,125チーム中478位
- 上位約7.8%
- Bronze Medal
- Public LB: 6.900
- Private LB: 8.952
でした。
最終的には、LightGBMだけではなく、地層を少しずつ追跡する粒子フィルターと機械学習を組み合わせるところまで試しています。
何を考えて、何を試して、何がうまくいかなかったのかをまとめておきます。
どんなコンペだったのか
今回予測するのは、地下を掘り進んでいるドリルが「地層の中のどこにいるか」です。 油田などでは、真下へ掘るだけでなく、一度地下まで降りたあと、地層に沿って横方向へ長く掘る水平井が使われます。
イメージとしてはこんな感じです。
地表
────────────────────
│
│
│
└────────────────→
水平に掘り進む
ところが、地下の地層はきれいに水平ではありません。 傾いたり、うねったりしています。
地層A / ̄ ̄ ̄ ̄ ̄ ̄
地層B /______
───────→ ドリル
そのため、ドリルの絶対的な深さが分かっても、
今、地層の中のどの高さを通っているのか?
は簡単には分かりません。
今回予測する TVT は、ざっくり言えばこの地層を基準にした位置です。
GRという「地下の波形」を手掛かりにする
面白かったのが GR というデータでした。
GR(Gamma Ray)は岩石が出す自然放射線の強さです。
岩石の種類によって値が変わるため、井戸に沿って測っていくと、
GR
│ /\ /\
│ /\ / \_____/ \
│_/ \/ \__
└──────────────────── MD
のような波形になります。
そして今回のデータには、水平井だけではなく、地層を縦に測った 参照縦井(typewell) のGRも与えられていました。
つまり、
水平井で今見えているGRの形が、参照縦井のどの部分に似ているかを探せば、現在の地層内の位置が分かるのでは?
と考え進めました。
最初に作った基準
まず、とても単純な予測を作りました。
評価対象区間の直前までは TVT_input として正解が分かっています。
そこで、
最後に分かっているTVTを、そのままずっと予測する
というBaselineを作りました。
既知区間 予測区間
──────●────────────────
↑
最後のTVTをそのまま使う
これでCV RMSEは 15.910。
当然強いモデルではありません。
ただ、今回かなり大事だったのは、
「最後に分かっているTVTから、どれだけ動くか」
として問題を考えられることでした。
TVTそのものをいきなり当てるより、
予測TVT
=
最後の既知TVT
+
その後の変化量
とした方が圧倒的に扱いやすくなります。
これは最後まで残った考え方でした。
最初は普通に機械学習してみる
まずは、
- MD
- X / Y / Z
- GR
- formation top
- 最終既知TVT
- そこからの距離
などを特徴量にしてLightGBMを試しました。
それなりには予測できます。
ただ、触っているうちに違和感が出てきました。
今回の1行と次の1行は、本当は独立ではありません。
同じドリルが、
地点1 → 地点2 → 地点3 → 地点4
と進んでいるだけです。
にもかかわらず通常のテーブルモデルでは、それぞれをかなり独立したサンプルとして扱います。
さらにGRも、
GR = 65
という一点の値より、
54 → 61 → 72 → 80 → 69 → 55
という波形そのものに意味があります。
ここから、普通の表形式回帰だけで解くのは少し違うのではないか、と考えるようになりました。
GRの波形を直接合わせてみる
そこで、水平井のGRと参照縦井のGRを直接比較する方法を試しました。
単純に考えると、
一番GRが似ている場所を選べばよい
となります。
ところが、これが思ったほど簡単ではありませんでした。
地下のGRには似た模様が何度も現れます。
例えば、
/\ /\
_____/ \____ _____/ \____
のような場所が二つあった場合、局所的にはどちらもかなり似ています。
一度ここで別の場所を選んでしまうと、
本当の位置 ─────────→
予測位置
─────────→
のように、いきなり別の深さへ飛んでしまいます。
RMSEなので、この手の大外しはかなり痛いです。
「一番似ている場所」を決めるのをやめた
ここで発想を少し変えました。
現在位置を一つに決めるのではなく、
この辺かもしれない こっちかもしれない まだこちらの可能性もある
と複数の候補を持ったまま進めればよいのではないか。
そこで使ったのが粒子フィルターです。
粒子フィルターでは地層内の現在位置について、例えば500個の候補を持ちます。
候補
•
••
• •
•
──────●──────── TVT
本命付近
ドリルが進むたびに候補を少し動かし、
GRが観測されると、
この位置ならGRが近い → 残す
この位置ではGRが違う → 重みを下げる
という操作を繰り返します。
これなら、一回GRが似ていただけで別の地層へワープしてしまうのを多少防げます。
ただしGRを信用しすぎてもダメだった
粒子フィルターを入れれば全部解決、とはなりませんでした。
特に効いたのが、
GRの一致を信用しすぎない
という調整でした。
GRが少し違うだけで候補を強く捨ててしまうと、偶然似ている別の場所へ粒子が集中することがあります。
そこで最終モデルでは観測誤差に下限を設け、
GRが少し違う
≠
その候補は絶対に間違い
としました。
最終的には観測誤差の下限を 45 にしています。
このあたりは、精密に合わせようとするより、むしろ少し鈍感にした方が安定しました。
今回のコンペでは何度も、
正確にしようとするほど壊れる
という現象に遭遇しました。
地質モデルだけでも足りないのでLightGBMと組み合わせる
とはいえ粒子フィルター単体も万能ではありません。
そこで最終的には、
地質的な連続性を追うモデル + 機械学習
という形にしました。
最終提出の098では、大まかに次の処理をしています。
- 最後に分かっている
TVT_inputを基準にする - 水平井と参照縦井のGRを粒子フィルターで対応付ける
- その追跡結果を特徴量化する
- 掘削経路、GR、地層上面などと合わせて196特徴量を作る
- LightGBMを井戸単位の5-foldで学習する
- 複数モデルの出力をRidge回帰で統合する
- 最後に急激なジャンプや細かな振動を抑える
粒子フィルターについては、128通りの乱数初期値と500粒子で計算しました。
機械学習だけで全部を学ばせるのでもなく、地質モデルだけですべてを決めるのでもありません。
人間側で、
TVTは掘削に沿って連続して変わるはず
という構造を入れ、その結果をLightGBMへ渡す形です。
今回一番うまくいったのはこの組み合わせでした。
CVは井戸単位で分割した
検証では同じ井戸の途中をTrainとValidationへランダムに分けてしまうと、ほぼ隣の地点を学習データとして見ることができます。
それでは本番より簡単すぎます。
そこで773井戸を丸ごと分けるGroupKFoldを使いました。
- 全foldで改善しているか
- 一部のfoldだけで劇的に良くなっていないか
- 最悪foldが悪化していないか
- 特定の井戸だけ大外ししていないか
も確認しつつ、最終モデル098の結果は次のようになりました。
| Fold | 井戸数 | 最終モデル | 粒子フィルター単体 |
|---|---|---|---|
| 0 | 155 | 8.945 | 10.539 |
| 1 | 155 | 8.805 | 10.130 |
| 2 | 154 | 8.252 | 10.294 |
| 3 | 155 | 8.169 | 8.808 |
| 4 | 154 | 9.428 | 10.542 |
| 全体 | 773 | 8.732 | 10.083 |
Baselineの15.910と比較すると、かなり改善しました。
うまくいかなかったもの
今回、試したけれど最終的に使わなかったものもかなりあります。
近くの井戸から地層を補間する
最初はかなり期待しました。
地下の地層は空間的につながっているので、
近くの井戸がこの傾きなら、この井戸も似ているはず
という考えでしたが、近隣井戸が十分ある場所ではうまくいっても、疎な地域へ行くと突然外れました。
GRのBest Matchをそのまま採用する
似た波形が複数存在するため、一度別の場所を選ぶと大きく外れます。
結果として、
一番自信がある予測を使う
より、
複数候補を残しながら追う
方が安定しました。
大きな系列ニューラルネット
GRが系列ならTransformerやRNNなどで全部学習すればよいのでは、という方向も試しました。
ただ、安定しませんでした。
773井戸というデータ量や、本番との分布差を考えると、大きなモデルに自由に学習させるほど簡単な問題ではなかったように思います。
少なくとも今回の試行では、地質的な構造を明示的に入れたLightGBM系の方が扱いやすかったです。
最終結果
最終結果は、
6,125チーム中478位、上位約7.8%、Bronze Medal
でした。
トップ層にはまだかなり距離があります。
普通の回帰
↓
系列として考える
↓
参照波形とのalignment
↓
粒子フィルター
↓
機械学習との統合
と少しずつ問題の形を変えながら追えたのは面白い経験でした。
今回一番学んだこと
このコンペで一番印象に残ったのは、
モデルを複雑にするより、まず「このデータは何なのか」を考えることの方が効いた
ことです。
最初は773井戸分のテーブルデータに見えていました。
でも実際には、
地下を進む一本の軌跡を、GRを観測しながら追跡する問題
でした。
そう捉え直すと、
- 行をランダムに分割してはいけない
- GRを一点の特徴量として扱うだけでは足りない
- TVTをゼロから当てる必要はない
- 現在位置を一点に決める必要もない
ということが見えてきます。
最終モデルで使ったLightGBM自体は特別珍しいものではありません。
大きく効いたのはその前段にある、
「どういう問題として解くか」
の部分でした。
昔から言われているような話ですが、実体験として得られたのは学びでした。
Public LBとの付き合い方も難しかった
もう一つはLeaderboardです。
Publicが上がると、当然そちらを信じたくなります。
今回もPublic 6.900まで上がったときはかなり期待しました。
しかしPrivateは8.952でした。
結果だけを見ると、井戸単位GroupKFoldの8.732の方が実際の性能をずっと正確に表していました。
もちろんCVも絶対ではありません。
ただ、
Publicが上がったから採用する
ではなく、
なぜ上がったのか説明できるか 複数foldで改善しているか 最悪ケースも改善しているか
を見る方が、最終的には安全でした。
これも昔から言われている話だと思いますが、自分のモデルで実際にPublicとPrivateが大きくずれると、かなり実感があります。
Appendix: 用語
| 用語 | 意味 |
|---|---|
| 水平井 | 地下へ降りたあと地層に沿って横方向へ掘る井戸 |
| wellbore | ドリルが通る三次元の掘削経路 |
| lateral | 水平井の主に横方向へ進む部分 |
| typewell | 比較のため地層を縦方向に測った参照井 |
| GR | Gamma Ray。岩石の自然放射線の測定値 |
| formation top | 地層が始まる深さ |
| dip | 地層の傾き |
| MD | 掘削経路に沿って測った距離 |
| X / Y | 地図上の位置 |
| Z | 固定基準から見た垂直方向の深さ |
| TVT | 地層を基準にした垂直方向の位置 |
| TVT_input | 予測開始位置までは値が分かっているTVT |
Z と TVT はどちらも深さのように見えますが、基準が違います。
Z が空間に対する深さなのに対して、TVT は地層に対する位置です。
今回予測していたのは後者でした。