テクノロジー

TGS Salt Identification Challenge 参加録

我々のチームでは業務の一環でKaggleに取り組んでおり技術力の向上、新たな手法の習得に努めています。今回紹介するのはKaggleのTGS Salt Identification Challengeコンペです。このコンペの期間は7/19~10/19で、この間にモデルを作り予測精度により順位を決定するという流れになっています。我々は4人でチームを組み、最終成績は3,234チーム中23位という成績でした。Goldメダルは16位からだったので惜しくも届きませんでしたが、不慣れなテーマの中でまずまずの成績を残すことが出来たと思っています。本記事では、コンペのお題についての解説と我々のチームの解法を紹介します!

コンペのテーマ

今回のコンペは地震探査によって作成した画像から塩領域を画像セグメンテーション技術を用いて予測するというものです。なぜそのようなことをするかというとどうやら塩の固まりが石油やガスなどの資源を囲うような構造になっていることが多く、そのため塩領域を地震探査で見分けることはとても大事であるそうです。

地震探査は人工的に地震を起こしてその反射波を地震計で捉えて地層の性質を捉える技術です。例えば、下のような画像が今回使用したデータに近いです。コンペの画像を外部に公開するのは規約上、危険な気がするので公開されているものを紹介します。

seismic

出典:https://www.ogpe.com/articles/print/volume-59/issue-12/upstream/two-fold-broadband-conventional-seismic-data-processing-technique.html

基本的な機械学習コンペではトレーニングデータとそれに対応する正解データと正解データが存在しないテストデータが提供されます。参加者がやることはトレーニングデータと正解データからモデルを作成し、そのモデルを用いてテストデータに予測を与えて提出します。画像セグメンテーションのタスクにおける正解データは今回は塩の領域、それ以外の領域のセグメンテーションなので左の画像に対して作ると右のようになります(この画像の塩領域が正しいとは限らないです。あくまでイメージが伝わるように私が適当に作りましたので注意)。正解データの実態は塩の部分が1,それ以外は0となっているものの2次元画像です。このような画像セットから今まで専門家が人力でやっていた塩領域検出を自動で出来るようにしたい、というのが今回のコンペのホストであるTGS社の狙いだと思います。

ではこれらのトレーニングデータ4,000枚を用いてどのように予測モデルを作っていくか紹介します。

画像セグメンテーション(U-Net)

画像セグメンテーションは古くから研究されている分野です。ただし、最近はこの分野も御多分に洩れずニューラルネットワークベースの研究が大多数を占めています。我々も画像=ニューラルネットくらいの知識なので、古い研究については調べていません・・・では、ニューラルネットでセグメンテーションを行う代表的なモデルというとU-Netだと思います。名前だけは聞いたことがありましたが、実際に使うのは今回が初めてでした。論文はこちらから。

モデルの構造は以下のようになっていてEncoder + Decoder構造になっています。Encoder側では畳み込みとMaxpoolingでより局所的な特徴量を作りながら一方でそのまま右側のDecoder層にスキップさせるのが特徴になっています。これにより元々の画像が持っていた位置情報と局所的な情報を合わせることが出来ます。

unet

実際にこの論文で紹介されている実験では次のようになります。

unet_seg

(a)が入力画像で(b)がU-Netの出力になります。このように見ると綺麗にセグメンテーション出来ていることがわかります。ちなみに画像セグメンテーションではU-Netよりも新しいMask R-CNNなどあります。ただし、今回のタスクでは使用されていなかったので我々も調べていません。参考までにMask R-CNNが利用されたKaggleコンペは2018 Data Science Bowlです。気になる方はそちらの上位陣の解法を追ってみると良いと思います。

今回のコンペで我々が使用したU-Netの構造は上のものとは若干異なっています。図示すると下のようになります。

unet-model

元々の論文実装ではEncoderは複数のConvolution層から構成されていますが、今回、我々はImageNetで事前学習したResNet34、ResNeXt50、SENet154を使用しました。事前学習モデルの中でフィルタ数が同じブロック群を1つのEncoderとして取り扱い、図中ではEncoder 1~4と表記しました。それぞれのEncoderの出力をDecoderの出力とConcatするのは論文と同様です。

元々のU-Netの構造を見るとDecoderの最後の出力のみ、セグメンテーションに寄与する形になっています。ただ、下層のDecoderも情報を持っているのでそれらの出力も使った方が精度が向上する場合があります。そこで使ったのがHypercolumnsです。今回はDecoder 1~4のそれぞれの出力をMaskのサイズまでUpsampleしたあとチャンネルの次元でConcatします。コードで書くと以下のようになります。

さらに、通常のセグメンテーションのタスクだけでなく補助タスクを解くことによって精度を向上させるDeeply supervisedの手法も取り入れました。最終的に6つのモデルを採用し、その設定を以下の表にまとめました。

Encoder Input size Padding Deeply-supervised
ResNet34 256 x 256 resize and reflect
ResNet34 256 x 256 resize
ResNeXt50 128 x 128 reflect ×
ResNeXt50 128 x 128 resize ×
SENet154 128 x 128 reflect
SENet154 128 x 128 resize

 

ここでPaddingは101×101の画像データをどのように128×128(256×256)にするかを表しています。resizeはそのまま目的のサイズにするだけですが、reflectはOpenCVのREFLECT_101を用いています。つまり端側の鏡映をとって画像サイズを128にしています。今回のセグメンテーションタスクは端側に少し塩領域があるような画像を精度良くセグメンテーションするのが難しく、そのため鏡映によって端側の塩領域面積を人工的に増やして精度を高めようと試みました。ちなみに256×256にする場合のreflectはまず202×202にresizeしたのちに、reflectして256×256にしています。

Deeply Supervised

Deeply Supervised モデルは元ネタの論文[1]を基に、Hengさんがコンペ用にアレンジしたモデルです。若干の精度向上と過学習抑制の効果があります。以下にDeeply Supervised モデルの概要を示します。

deeply-supervised

Deeply Supervised モデルは通常のセグメンテーションのタスクだけでなく、ターゲットが存在するかしないか分類するタスクと、ターゲットがある場合のみセグメンテーションを行うタスクの3つのタスクを並行して解きます。通常のセグメンテーションの問題ではピクセル単位でターゲットの予測を行いますが、画像全体の情報も合わせて学習を行うと精度が向上することが過去の研究で分かっています [2]。今回のコンペのデータセットは1つの大きな3D画像から切り出されたものであるため、画像の中にまったくターゲットが存在しなかったり、画像の端のほうにだけターゲットが存在したりする場合が多くあります。そのため、画像単位でターゲットが存在するかしないかを分類するタスクと組み合わせることで精度が向上することは直感的にも理解しやすいと思います。もう一つ特徴的なのは分類タスクで使った特徴量とセグメンテーションで使った特徴量をConcatするところです。こうすることで分類タスクとセグメンテーション両方を考慮したより高度な表現ができると考えられます。

モデルについてもう少し詳しく見ていきましょう。ターゲットが存在するかしないかを判断する2値分類タスクではマスクを生成する必要がないため、Encoderの出力を特徴量にします。ターゲットが存在するときのセグメンテーションはHypercolumnsを特徴量とします。最後にHypercolumnsとEncoderの出力をConcatしたものを特徴量として最終的なモデルの出力とします。最終出力ではHypercolumnsとEncoderの特徴量を組み合わせることで、ターゲットがまったく存在しない場合のセグメンテーションの精度向上を狙います。

以下にDeeply SupervisedのPytorchの実装例を示します。

まず、各レイヤを定義します。fuse_pixelはHypercolumnsを入力としてセグメンテーションの特徴量を算出します。logit_pixelfuse_pixelを入力としてターゲットが存在するときのセグメンテーションを行います。fuse_imageはEncoderの出力の特徴量を計算します。logit_imagefuse_imageを入力としてターゲットが存在するかしないかの2値分類の結果を出力します。logitfuse_pixelfuse_imageをConcatしたものを特徴量として、すべての画像に対するセグメンテーションの結果を出力します。

次にforward関数の記述例を示します。

ここでdはHypercolumnsをe5はEncoderの出力を表しています。logitにはHypercolumnsとEncoderの特徴量をConcatしてUpsamplingを行ったものを入力します。Encoderの特徴量はGlobal Average Poolingの結果、size = (Batch size, channels, 1, 1)となっているのでUpsamplingを行う必要があります。forward関数の出力は3タスクを並列で解くためlogitlogit_pixellogit_imageの3つとなり、それぞれの出力結果に対して損失関数の最適化を行います。

損失関数の設計

Deeply supervised モデルでは3つのタスクを並列で解きます。それぞれのタスクについて損失関数を設定して同時に最適化を行います。実装では、それぞれのロスに重みを乗じて足し合わせたものを最終的なロスとしてOptimizerに入力します。入力の画像サイズを変更していますが、 paddingを行った場合はpadding領域を除いた結果のロスを計算します。以下に設定した損失関数を示します。ここでWeightはハイパーパラメータとなりますが、値はHengさんの設定と同じとしました。

Task Loss Weight
2値分類 Binary Cross-Entropy 0.05
セグメンテーション(ターゲットあり限定) Lovasz hinge 0.10
セグメンテーション(すべての画像) Lovasz hinge 1.00

Lovasz Hinge Loss

Lovasz LossはIoUを最適化するために考案された損失関数です [3]。今回はターゲットが存在するかしないかの2値セグメンテーションの問題であるためLovasz hinge lossを使用しました。Lovasz hinge lossの内部の計算ではヒンジ関数を用いますが、論文実装ではmax-marginの計算にReLUを用います。Hengさんの調査によると、この部分をelu + 1に変更すると精度が向上するとのことで、我々もそちらを採用しました。以下にReLUとELUの関数の違いを図で示します。

elu

elu + 1 に変更した場合、正解ラベルでも分類境界面に近い場合は損失が発生します。セグメンテーションが難しいピクセルにおいて重点的に学習する効果が期待できると思われます。ただし、学習が収束するときのロスの値は上昇します。

ちなみにbackwardを呼んで重み更新する際にPaddingのタイプによって多少の工夫を行いました。単純にinputをresizeしたときはニューラルネットの出力をそのまま用いて計算したロスの値によってbackwardをかけます。一方でreflectした場合は、例えば101×101 → 128×128の時、NNの出力は128×128になりますがbackwardをかけるときは元々のinput領域、つまり101×101の部分のみでロスを計算し重み更新するように変更しています。ResNet34で実験した際にはこの方法で学習した方がPublicスコアが0.002向上しました。

Augumentation

トレーニング時に以下の4つのAugumentationを実施しました。ここでpは1枚のトレーニング画像に対して各メソッドを実行する確率です。本コンペは地震探査によって作成された特殊な画像でしたが各種Augumentationが精度向上に繋がりました。Horizontal Flipのみの場合とすべて実施した場合で比較するとIoUが0.01程度改善されます。

Method Parameters
Horizontal Flip p=0.50
Random Crop and Resize p=0.07, limit=0.2
Elastic Transform p=0.07
Shift Scale Rotate p=0.07, shift_limit=0, scale_limit=1, rotate_limit=10

Depth information

今回の画像は地層内部の構造をイメージングしたものなので、画像内でDepthの情報を入れると精度が向上したというDiscussionがありました。これは画像を101×101の行列として見たときに1行目が最も地表に近く、101行目が最も深いだろうと考え、入力画像に深さを表す擬似的な値を組み込むというアイデアです。
バッチ毎に処理したのでコードは次のようになります。

結果

今まで説明したものを取り入れて実際にsubmitした時のPublicスコアを表にすると次のようになります。

Encoder Public Score(8fold Ensemble)
ResNet34 0.866
SE-ResNeXt50 0.864
SENet154 0.867

 

今回は塩のカバレッジによって8クラスに分けて、そのクラスでStratifiedKFoldにより8foldに分割しています。上記の表のスコアは全てfold毎に学習し、それぞれTTA(flipのみ)を行い8つのpredictの平均をピクセル単位でとって作成したsubmitファイルの結果となっています。細かく説明すると各foldの学習においてもSnapshot Ensembleを6サイクル行い、それぞれバギングしています。Snapshot Ensembleは学習率を次の式によって変化させ学習させます。

cyclic

T=300, M=6(サイクル数)とすると

snapshot

のような形状になります。我々もこの論文と同じ設定で行いました。つまり最終的には1モデルにつき、48(6×8)個の出力を利用してそのモデルの最終結果としています。このSnapshot Ensembleを行うことによって各モデルのスコアは0.002程度改善しました。

以上の3モデルをバギングし、Public:0.8698、Private:0.8884という結果となりました。StackingやPseudo labellingのような半教師学習を使おうと初期から考えていましたが想像していたよりモデルの学習に時間がかかり手が出せずに終わってしまったのが心残りです。また、Optimizerは一貫してSGDを採用していましたが上位陣はAdamWを使っていたり、セグメンテーションでは役に立たないと勝手に決めつけていたCutoutを用いていたなど、この辺りももう少し実験した方が良かったと後悔しています。kaggleの画像系コンペに参加したのは初めてでしたが、テーブルデータコンペよりも最新の論文をどのくらいキャッチアップしているか、また面倒くさがらずに色々な手法を試すということがどこまで出来るかが勝敗を分けると感じました。

その他 調査したモデル

検討したものの採用には至らなかったモデルを記録しておきます。

Method Reported from Note
Attention U-Net Oktay et al. (2018) U-Net のSkip-connectionにAttentionを適用したモデル
Deeplab v3 Chen et al. (2017) ResNet BlockにAtrous Convolutionを導入 ASPP(Atrous Spatial Pyramid Pooling)を提案
Large Kernel Matters Peng et al. (2017) GCN(Global Convolution Network)を提案 k x k畳み込みの近似を使って精度を保ちつつパラメータを削減した
OCNet Yuan et al. (2018) Self-Attentionを使ったネットワークの提案
Pyramid Attention Network Li et al. (2018) EncoderのGlobal Average PoolingをAttentionとして利用するGlobal Attention Upsampleを提案

体制面

最後にチームで戦う上でどのような工夫をしたかを書いておきたいと思います。まずチームで開発するため当然コードは全てgit管理しました。使ったサービスは弊社の業務でメインで使っているのでbitbucketを利用しました。運用はGitFlow的なものにしています。これも慣れているからという理由ですが、システム開発ではないのでreleaseブランチは除いた運用になっています。各々の実験はdevelopからfeatureブランチを切って行い、精度が向上したり実験を回す上で便利な機能(ログの可視化など)が追加された場合はプルリクエストを出してdevelopで検証、そこで問題なければmasterへマージするという流れで進めました。最終的に阿部がソロで参加していた時のコミットも含めると119コミット、チームマージ後の3週間程度でプルリクエストは13でした。そしてどういった実験・機能追加をするべきかということを決定する手段ですが、bitbucketの課題で管理することも考えましたが結局Trelloを使うことにしました。これも業務で使ったことがあってメンバーの進捗管理をするのがとても便利なのでコンペでも使ってみました。細かく工数管理などしませんでしたが、現在誰がどんな実験をしていて進捗は大雑把にどうなっているか全員が把握できたので限られた時間内で効率的に取り組めたと思います。業務時間外や休日のメンバーとの連絡はslackを用いました。(チームメンバーの吉田が学習ログをslackに飛ばすような機能を作っていましたが、これは精神的に良くないので結局使いませんでした・・・)

このような環境が最適なのかわかりませんが、kaggleをチームで取り組む際のノウハウのようなものがあまり転がっていないのでちょっと書いてみました。金メダルに届かなかったので記事の価値としては微妙なところですが、本記事が誰かの役に立てば幸いです。ここまで読んで頂きありがとうございます!


関連タグ