📷

kaggle IMC2022参戦記

2022/06/12に公開

はじめに

2022/4/4〜2022/6/3まで開催していたImage Matching Challenge 2022というコンペに参加し、優勝することができました。
https://twitter.com/yume_neko92/status/1532667439258882049
最終的なソリューションなどは公式のディスカッションに公開しましたが、どういう試行錯誤を経てこのアイデアに辿り着いたかというところを備忘録も兼ねてこの記事で公開したいと思います。

自己紹介

コンペの話の前に、簡単に自己紹介をしたいと思います。
筆者のバックグラウンドは大雑把にこんな感じです。

  • Twitter @yume_neko92
  • 業務でちょいちょい画像系の深層学習には触れている
    • でも今回のコンペの分野(画像マッチング)はほぼド素人
  • kaggle歴は半年くらい
    • IMC2022は4回目の参加コンペ
    • なんとなくkaggleでの戦い方が分かってきたレベル

どんなコンペ?

ざっくり言うと、同じ建物を撮影した2枚の画像が与えられたときに、それぞれの画像を撮影したカメラの相対位置関係(F行列)の推定精度を競うコンペでした。
F行列を求める方法はいくつかありますが、基本的には2つの画像のマッチング点(同じ物体がそれぞれの画像に映る点)を求めて、RANSACで外れ値を除去して妥当なF行列を計算するという流れになります。外れ値の除去やF行列の計算はOpenCVのcv2.findFundamentalMatで行うことができるので、いかに上手くマッチング点を求めるかというところがこのコンペでは重要な部分になっていました。
もっと詳しいコンペの解説は@fam_taroさんが公開してくださっているQiitaの記事がとても分かりやすいので、興味がある方はぜひそちらの記事を見ることをオススメします。
https://qiita.com/fam_taro/items/bdc47e944d4612a0eac1#21-local-feature-matching-cv2findfundamentalmat

1stソリューションの要点

大まかなソリューションの流れは以下のような感じです。

  1. 通常画像でマッチング
  2. 得られたマッチング点をクラスタリングして2枚の画像の共通部分をクロップ
  3. クロップした画像同士で再マッチング
  4. 1で得られた通常画像のマッチング点と3で得られたクロップ画像のマッチング点を連結してF行列を計算

特にスコアアップに大きく関わったキーポイントは以下の3つです。

  • 共通部分のクロップ(mkpt crop)
  • 複数モデル(LoFTR、SuperGlue、DKM)のアンサンブル
  • 入力解像度を複数使用

詳しい解説は公式のディスカッションに公開しているので、もっと詳細が知りたい方はぜひこちらを覗いてみてください。

コンペ参戦記

  • 前置きが長くなりましたが、ここからがこの記事を書いた目的になります
  • 最終的なソリューションはすでに紹介した通りですが、コンペ中にどんなことを考えて試行錯誤していたか紹介できればと思います

1. コンペ参加〜基礎知識の勉強(4/25〜4/27)

  • 4月末まで参加してたコンペが終わって、次のコンペを漁っている時にたまたま見つけました
    • 画像マッチングは未知の領域でしたが、たまたま興味が湧いていたのと、言っても画像分野なのである程度戦えるだろうと思い、勉強の一環として参加することにしました
  • 手始めにディスカッションを片っ端から読んで、コンペの概要やよく使われるモデルなどを理解しました
  • ただディスカッションだけだとカメラ幾何がよく理解できなかったので、並行してwebで参考になりそうな記事を読み漁ってました
    • 特にこちらのQiita記事がとても分かりやすく、F行列のイメージをつかむことができました。

2. CV検証環境の準備〜初サブミット(4/28〜5/5)

検証環境を作成するのが何より大事ということは過去コンペで身に染みて分かっていたので、まずは公開されていた検証用ノートブックを解読して検証環境を作成しました

  • 手始めに当時ベストスコアのノートブックの変更点を反映してCVスコアを出してみたのですが、なぜか著しくCVスコアが低くなるという現象にぶち当たります

    • 原因は参考にしていたノートブックに足りない処理(リサイズした画像同士で求めたマッチング点をリスケールせずにF行列計算していた)があったことでした
    • 当時はすぐに気づけず色んな実験を繰り返してディスカッションに投げてました
    • その時のやり取りはここで見れるので、興味のある方はご覧ください
    • ここでだいぶ時間を使ってしまいましたが、マッチング結果を確認する簡易UIとか、その後の検証でも使えるコードを整備できたので、このタイミングで色々実験していたのは結果的に良かったと思います
  • リサイズの部分を修正したら妥当なCVスコアが出たので、修正版ノートブックをサブミットしました

    • あまり期待していませんでいたが、当時の銀圏スコアが出て喜んでました
    • ここで運よく良いスコアが出たので本腰を入れて取り組み始めました

3. 仮説を立てる(5/6〜5/7)

ベースラインができたので、次にどうやったらスコアを上げられるか仮説を立てて検証を進めることにしました。

  • 仮説1: スコアが低い画像はマッチングの誤対応が多い → (結果△)

    • スコアが低い画像ペアには何かしら共通点があるだろうという見込みから簡単な仮説を立てました
    • スコアごとに画像ペアを集めてマッチング点を一つずつ目視で確認して検証をしました
    • 結果は微妙で、たしかに誤対応もありましたが割合はそれほど多くなかったです(ただ、この発見は後々のアイデアに活きてきて優勝のキーになりました)
    • 実際のマッチング結果を見てもスコアの良し悪しと結果の差が自分には分からなかったので、スコアが低いペアから共通点を見出すのは一旦諦めました
  • 仮説2: 誤対応点を削除すればスコアが上がる → (結果○)

    • 仮説1の検証の結果、スコアの大小に関わらず誤対応をしているケースは見受けられたので、手動で誤対応点を削除してスコアをいくつか出してみました
    • 結果は例外はあるものの基本的には不要なマッチング点を削除するとスコアが上がる傾向を示しました

4. 仮説に基づいて改善案を検討〜高速化(5/8〜5/15)

検証の結果、見込みがありそうだった仮説2に基づいて改善案を考えることにしました。

  • 改善案1: スコアが低いマッチング点を足切り → (結果×)

    • 当時使用していたマッチングモデル(LoFTR)は各マッチング点の信頼度も出力していたのですが、外れ値は信頼度が低いと仮定して、信頼度の低い点を削除してみました
    • 結果は全然ダメでスコアが下がりました
      • そのあとの検証で分かりましたが、信頼度と外れ値かどうかにはあまり相関が無かったので、LoFTRの信頼度をベースに改善をするのは悪手だったと思います
  • 改善案2: NMSでマッチング点を間引き → (結果×)

    • まだ信頼度の検証をする前で、再び信頼度ベースの改善を試してました
    • 改善案1と同じ理由で上手くいかなかったです
  • 改善案3: セグメンテーションでクロップ → (結果○)

    • 改めてマッチング結果を確認してみて、誤対応している多くは空や道路、人など本来一致しないはずの領域にマッチングしているパターンが多いことに気付きました
      • セグメンテーションして不要な領域をあらかじめ除外すれば誤対応を抑制できるのではと考えます
    • Mask2Formerを組み込んでサブミットしましたが、処理に時間がかかりすぎてTimeOutErrorになってしまう壁に当たりました
      • 全体的にかなり適当な実装をしていたので、一旦ここで最適化することにしました
      • ボトルネックになってそうなところを片っ端から潰していったら約3倍くらい高速になりました
      • ちなみにこの段階でRANSACのパラメータも精度がなるべく変わらずに高速になるよう調整したのですが、その後の検証も高速に回せるようになったので早めに調整して良かったと感じてます
    • TimeOutが回避できるようになり、無事スコアアップすることができました(0.785で銀圏中〜上位くらい)
      • ただ、最初からスコアアップしたわけではなく、いくつか調整が必要でした
      • 最初は不要な領域にマスクをかけた画像でマッチングをしてみたのですが、これだと大幅にスコアが下がっていました。おそらく、マスクをかけた時にオブジェクトのエッジ部分の情報を欠落させてしまったためだと思われます。
      • そこで不要領域をマスクした画像ではなく、建物部分だけをクロップした画像でマッチングする方式に切り替えてスコアアップを実現しました
      • ちなみに最終ソリューションでも採用しているオリジナル画像のマッチング結果もアンサンブルするというアイデアはこの時点で思い付きました
        • 小さかったり複雑な形状をしているオブジェクトは上手くセグメンテーションできないことは学習データに対する結果で分かっていました
        • セグメンテーション自体の性能向上をすることも考えましたが、少し試してみて一筋縄ではいかなそうなこと、そもそもコンペの本質から外れる気がしたのでこの方針は早々に止めました
        • オリジナル画像のマッチング点を加えると誤対応点が増えるので仮説の方針と反するとも思いましたが、クロップ画像のマッチング点のおかげで正常対応点の割合が増えるので多少の誤対応は RANSACが弾いてくれるのでは?と思い、オリジナル画像のマッチング点を加えることを試してみました
        • 結果、だいぶスコアが良くなったのでオリジナル画像を組み合わせる方針を採用していました

5. アンサンブル導入〜金圏突入(5/16〜5/19)

  • セグメンテーションクロップの調整がひと段落して手持ちのアイデアが無くなったので、ディスカッションで紹介されていたIMC2021の上位者が公開している論文を読み漁ってました
  • その中で紹介されていたLoFTRのマッチングにSuperPointを組み込む工夫が気になって実装にチャレンジしました
    • が、上手く実装できずLoFTRにSuperPointを組み込むのは早々に諦めます
    • ただ、実装に挑戦してる中でSuperPoint+SuperGlueの公開ノートブックを読み込んでいたので、せっかくなのでモデルをアンサンブルしてみることにしました
    • このアンサンブルがかなり効果を発揮して当時のギリギリ金圏スコアを出せました(0.816くらい)
  • その後、SuperGlueのパラメータをちょこちょこいじってスコアを微増させてました(0.821くらい)
    • 下手に金圏に入ったことで「順位を落としたくない」という心理が働いてパラメータ調整に執心していましたが、今思い返すとこれは悪手だったかなと思います
    • 基本的にパラメータ調整で可能な上げ幅はたかが知れてるので、最上位と越えられない壁を感じたら自分が気づいていない何かがあるはずなので、それを探すことに注力した方が建設的だったかなと思います

6. 新たな改善案検討〜金圏復帰(5/20〜5/26)

パラメータ調整に必死になっていると、案の定銀圏に落ちました。
「最後に上位にいることが大事」と自分に言い聞かせて、新たな改善案の検討に着手します。
改めてマッチング結果や今までの実験結果を振り返って、2つの課題を洗い出しました。

  • 課題1: クロップしても無駄な領域が多い

    • 改めてクロップした画像を見ていたところ、写ってる範囲が違う画像ペアだとクロップしても無駄な領域があることに気付きました
    • 例えば↓みたいな画像ペアの場合、左の画像は右の画像よりも写っている範囲が広いので、建物部分だけをクロップしても無駄な領域が残ります
  • 課題2: セグメンテーションに時間がかかる

    • ある程度高速化はしたものの、どうしてもセグメンテーション計算の部分で2時間程度かかってしまうことがネックになっていました
    • 特にアンサンブルをすると必要な処理時間が増えるので、セグメンテーションだけで数時間食ってしまうのはかなり痛い状況でした

これらの課題を解決するために無い知恵を絞って改善案を捻り出します。

  • 改善案4: mkptクロップ
    • 課題1の対策を考えているときに、最初の仮説検証でオリジナル画像でマッチングすると、誤対応はあるもののその割合はあまり高くないことを思い出しました
      • 改めてオリジナル画像に対するマッチング結果を見ると、多少外れ点はあるもののペアに共通している領域にはマッチング点が密集していることを確認できました
      • ここで、この外れ値をどうにか除去できればマッチング点の位置情報を元により正確なクロップができるんじゃないか?と思い始めました
    • 二次元座標の外れ値検出手法をいくつか調べて適用してみたところ、DBSCANを使うとかなり理想的に外れ値を除去することができて、セグメンテーションよりも高精度に共通領域をクロップできるようになりました
      • 狙っていたわけではないですが、DBSCANはかなり高速で、課題2も同時に解決できました

また、検討の中の副産物でこの時mkptクロップの他に2つ新しい工夫点を見つけていました

  1. 入力解像度を複数使用
    LoFTRのマッチングが原理上8pix単位でマッチングされるので、解像度を変えて組み合わせればより多くのマッチング点が得られるのではと思い試していました
  2. DKMとのアンサンブル
    前にアンサンブルで使っていたSuperGlueは(少なくとも自分が設定していたパラメータでは)処理時間がかかっていたので、ディスカッションで話題に上がっていたDKMを試していました。SuperGlueと精度はあまり変わらなかったですが、処理時間が速かったのでDKMを採用していました。

その他、細々した調整を繰り返して、スコアが0.83程度まで上がり金圏に戻りました。
といっても、上位は0.84代に乗せていたのでまだ大きな壁があるような状況でした。

7. チームマージ〜コンペ終了(5/27〜6/3)

  • ギリギリ金圏に入っていたものの残り1週間戦えるほどのアイデアは持ち合わせてなかったので、お声がけいただいた順位の近いチームとマージさせていただくことにしました

    • 相手チームは特に前処理などは行っておらず、様々なモデル・入力解像度を試してスコアを上げていました
  • 最初はお互いのソリューションを自分のソリューションに組み込みつつ、気づいたことを共有するスタンスで進めていました

  • mkptクロップの挙動を確認している中で、そもそもLoFTRが見つけられない(マッチングしづらい)領域があることが分かり、SuperGlueのマッチング結果もクロップ計算に使用したら精度が上がるんじゃないかと仮説を立てました

    • 共有すると早速チームメイトが実装をしてくれて、0.848で当時の金圏中位くらいのスコアを出すことができました
    • それまでは大幅なスコア改善ができていなかったので、自分のアイデアがスコアアップの役に立って少しホッとしていました
  • 0.848が出た次の日に別のチームメイトが使用するモデルと解像度を変更して、0.861というスコアを出しました

    • 当時のトップが0.85ちょいくらいのスコアだったので、結果が出たときはチームみんなで驚いてました
  • その後は0.861のノートブックをベースに、mkptクロップの改善案の検討とより良いモデル構成の検討に分担して進めていました

    • 自分はmkptクロップの改善に注力していて、クロップ結果を眺めて仮説を立てて実装してサブミットをすることを繰り返していました
    • 試していた工夫はこんな感じです
      • DBSCANのパラメータを動的に決定する
      • クロップ領域にパディングを加える
      • クラスタの選択方法を改良
      • ゆるい条件のクロップ領域画像でのマッチング結果をアンサンブル
    • 詳細は割愛しますが、いずれもひたすらクロップ結果を眺めて問題点の仮説を立ててその改善案を検討して実装していました
    • 実際にパブリックスコアを上げられたのは、試した中のごく一部でしたがコンペ終了後にプライベートスコアを見ると効果的だった工夫もいくつかありました
  • 最終的にスコアを0.863まで伸ばしてコンペ最終日を終えました

  • 結果発表直前は気が気じゃなかったですが、無事順位を維持することができて優勝することができました

    • 自分たちのソリューションはかなりmkptクロップに依存していたので、プライベートデータセットがmkptクロップと相性が悪かったら大幅にshake downするかもと不安に思っていましたが、そんなことはなくて本当に良かったです

まとめ

メモ書きのようになってしまって読みにくい部分もあったかもしれませんが、ここまで読んでいただきありがとうございます。
今回このコンペに参加して、ちゃんとデータや結果を見て何かしらの仮説をちゃんと立てて改善案を考えていくサイクルが大事かなと感じました。今までは上位ソリューションを読んでどうやってその解法に辿り着いているのかなと不思議に思っていましたが、意外と簡単な仮説からサイクルを回して改善を重ねていくのが近道なのかもしれません。(中には一発で最適解を引き当てる天才的な方もいるのかもですが)

優勝までできたのは運とチームメンバーに恵まれたおかげだと思いますが、これからも今回のコンペで学んだことを活かしつつ地道にGM目指して頑張っていきたいと思います。

Discussion