Kaggleの画像コンペに初めて挑んでみた

はじめに

Kaggleの雲コンペ「Understanding Clouds from Satellite Images」に参加し見事惨敗したため、ここに反省と備忘録を残します。

f:id:sinchir0:20191120221548p:plain
コンペが終わったときの顔

Understanding Clouds from Satellite Images https://www.kaggle.com/c/understanding_cloud_organization

間違っている部分があったら指摘頂けると嬉しいです。

概要

  • 初の画像コンペ

  • 1か月程度参加

  • 70submit

  • クラウド料金2万、合計立ち上げ時間約200h

  • LastSubmitのモデルは下記

    • Model1:Unet(Resnet18,34, Inceptionresnetv2のensemble) LB:0.6624

    • Model2:Unet++(EfficientNetB4) LB:0.6639

    • Model1,Model2の出力マスクのnp.logical_or LB:0.6636

    • HorizontalFlip,VerticalFlip,GridDistortion,ShiftScaleRotate

    • RAdam

    • Dice loss

    • TTA (HorizontalFlip,VerticalFlip,Vertical shift)

    • Single Fold

    • Remove false positive masks with classifier

    • 怪しいデータをTrainから削除

  • メダル圏外の悲しい結果でした

  • 画像コンペの基本的な部分は理解出来たため、次からは良い成績を残したいです。

分析環境

画像コンペは、画像処理に伴う並列計算が多く、GPUが必須となります。 GCPAWS上で環境構築する方が多いかなと思いますが、個人的にはPaperspaceがお勧めです。

クラウドに自分だけのコンピュータを!Paperspaceを使ってみた!
www.fusenstage.com

pytorch設定済のGPU環境が、ボタンポチポチすれば数分で構築出来て、
非常に簡単です。

ただし、JupyterNotebookを使う場合は、定期的にstorage配下以外のデータが削除されるため注意が必要です。

(一度、storage以外でデータ管理を行い全データ削除の憂き目にあっています…)

f:id:sinchir0:20191120080720p:plain
storage配下以外のデータは定期的に削除される
  また、計算時間が短い場合はGoogleColaboratoryを用いて計算していました。 colab.research.google.com

コンペ概要

自分で書こうかなと思っていたのですが、とても分かりやすい記事が投稿されていたため紹介します。

yukoishizaki.hatenablog.com

簡単に言うと、雲の形が4種類のどれか( Sugar(砂糖), Flower(花), Fish(魚), Gravel(砂利))を当てるSegmentationのタスクです。

f:id:sinchir0:20191121021044p:plain

Segmentationとは

僕がこのレベルからのスタートだったため、簡単に説明します。 画像コンペには大きく3つのタスクが存在します。

  • Classification

  • Detection

  • Segmentation

下記ブログが分かりやすかったため、画像をお借りして説明します。 starpentagon.net

Classification

f:id:sinchir0:20191121220222p:plain
該当の画像が何なのかを予測します。 上記の例だと、出力はbirdtoyとなります。

Detection

f:id:sinchir0:20191121220245p:plain
該当の画像の中で、対象物がどこにあるのかを矩形範囲で予測します。 出力は上記画像の矩形範囲となります。

Segmentation

f:id:sinchir0:20191121220307p:plain
該当の画像の中で、対象物がどこにあるのかをピクセル単位で予測します。 出力は上記画像から、元の画像を除いたもの(マスクと呼びます)となります。

今回はSegmentationのコンペであるため、提出ファイルは各画像に対するマスクのファイルとなります。

前半2週間の過ごし方

初めての画像コンペへの取り組みだったため、本当にDiscussion読んでもEDA読んでも何言ってるのかさっぱり分からんって感じでした。

f:id:sinchir0:20191120213421p:plain
なにがなんだかわからない僕

そのため、わけのわからん単語を一つ一つ検索したり、実際に自分でコードを回して挙動の確認を繰り返しました。

その時期に、kernel含め、ひたすら参考にしたWebサイトを下記に貼ります。

「kernel」

EDAは下記のkernelを中心に読みました。非常に分かりやすい。

www.kaggle.com

「Keras Documentation」

keras.io

NN系ライブラリ、Kerasの公式のF&Qです。ここでの学びが一番多かった気がします。

例えば、モデルのセーブの仕方、ロードの仕方はここで知りました。 

from keras.models import load_model

#モデルのセーブ
model.save('my_model.h5')

#モデルのロード
model = load_model('my_model.h5')

「albumentations」

albumentations.readthedocs.io

DataAugmentationのライブラリである albumentationsの公式サイトです。

DataAugmentationとは、学習データの画像を縦に回転させたり、輝度を変えたりして データを増やす手法です。

下の例はDataAugmentationの一つ、HorizontalFlipという手法で、横方向に回転させた画像を、学習データに増やした場合です。

f:id:sinchir0:20191121004239p:plain
HorizontalFlipの例

モデルに「似ているけれどちょっと違う画像」を学習させることで、 元々の学習データに極度に合わせに行くことを防ぎ、過学習を防止することが目的です。

そのDataAugmentationが簡単に行えるライブラリがalbumentationsです。 上図のHorizontalFlipなら下記コードが重要な部分となります。

augmentation = albu.Compose([albu.HorizontalFlip(p=0.5)])

ちなみにpは学習時にHorizontalFlipを行うかどうかの発生確率です。 p=0.5の場合は、HorizontalFlipを各画像に行う確率が50%の状態です。 1epoch毎に全ての画像データに対して、HorizontalFlipを行うかどうかが判定されます。

「過去コンペ振り返り」

初めての画像分類コンペでめっちゃ頑張って上位まで行ったが、閾値を攻め過ぎて大爆死した tawara.hatenablog.com

壷コンペ振り返り nmaviv.hatenablog.com

APTOS2019参戦記録 icebee.hatenablog.com

偉大な先人たちがコンペの振り返りを残してくれているので、何度も見返しました。(大大感謝です。)

後半2週間の過ごし方

  モデル構築と精度確認をメインで行いました。下記のUnetを用いたkernelをベースに取り組みました。

www.kaggle.com

Unetとは

一言でいうと、Segmetationが得意なネットワークです。 ちゃんとした理解は出来ていませんが上記ぐらいの認識で最初はいいんじゃないかと 自分に言い聞かせてごまかしています。

下記記事が分かりやすかったです。

lp-tech.net

このUnetをベースに下記のようなことに取り組んでいました。

Backboneの変更

Backboneとは、Unetのネットワークの一部であり、入力画像の特徴を抽出する役割を持つ部分です。 下記記事の概念が分かりやすかったため紹介させて頂きます。

qiita.com

下記画像の1.Backboneの部分が該当します。

f:id:sinchir0:20191121122910p:plain
Backboneのイメージ(画像はFaster R-CNNのネットワーク)

このBackboneには様々な畳み込み用のネットワークを適用できます。 例えば、VGG,Resnet,Densenetなど。

コードは下記のようなイメージです。

import segmentation_models as sm

#Backboneの指定
BACKBONE = 'resnet34'
#Backbone用のpreprocessを実施
preprocess_input = sm.get_preprocessing(BACKBONE)

# データのロード
x_train, y_train, x_val, y_val = load_data(...)

#preprocessの実施
x_train = preprocess_input(x_train)
x_val = preprocess_input(x_val)

#モデルの指定
model = sm.Unet(BACKBONE, encoder_weights='imagenet')

上記コードは下記サイトのライブラリを使用しています。 github.com

このBackboneを変更し、アンサンブルをすることでモデル汎化性能が出ることを期待しました。

Data Augmenationの変更

前半2週間で説明したData Augmenationについても、 下記の手法の中から、手法を足したり引いたりして 精度がどう変わるかを確認しました。

albu.HorizontalFlip(p=0.5),
albu.VerticalFlip(p=0.5),
albu.ShiftScaleRotate(rotate_limit=30, shift_limit=0.1, p=0.5),
albu.Rotate(limit=20),
albu.GridDistortion(), 
albu.Flip(),
albu.RandomBrightness()

Thresholdの変更

予測結果は確率で出力されるため、どの値を1,0を分けるかの閾値を決める必要があります。 予測対象毎に閾値を決定でき、今回は雲の形が4種類の4クラス分類のため、 best_tresholds = [.5, .5, .5, .5] と記載されていた場合には 「予測確率が50%以上のピクセルのみ1にするよ」という意味になります。

このThresholdを色々いじっていました。

epochの変更

一つの訓練データを何回繰り返して学習させるかの回数です。 この値も増やしたり減らしたりして精度がどう変わるかを確認しました。

TTAの実施

TTAとはTest Time Augmentationの略です。

Data AugmentationをTrainデータだけでなく、Testデータにも実施することを指します。

例えば、A.jpgという画像あったら、

  • A.jpg(元々の画像)
  • A-HorizontalFlip.jpg(Aに対してHorizontalFlipをした画像)
  • A-VerticalFlip.jpg(Aに対してVerticalFlipをした画像)

のようにデータを増やし、

  • A.jpgの1番目のpixelの推論結果:0.70
  • A-HorizontalFlip.jpgの1番目のpixelの推論結果:0.50
  • A-VerticalFlip.jpgの1番目のpixelの推論結果:0.65

  • A.jpgの1番目のpixelの推論結果:(0.70 + 0.50 +0.65)/3 = 0.62

と予測結果を平均します。一種のアンサンブルかと思います。

下記ブログの画像が分かりやすかったため紹介します。

f:id:sinchir0:20191121225112p:plain
TTAのイメージ

qiita.com

PostProcessing

PostProcessingは「後処理」という意味です。モデルを学習させ出力した予測結果に対し、 更に行う処理のことを表します。

僕の場合は、各画像をClassificationして、どの画像か( Sugar(砂糖), Flower(花), Fish(魚), Gravel(砂利))の閾値を出力し、 その閾値を下回ったマスクを削除するPostProcessingを行っていました。各kernelをベースにさせて頂きました。

www.kaggle.com

アンサンブル

単純に、各ピクセルごとの予測結果の確率の平均を取りました。 アンサンブルは下記のような感じです。

  • Model1:Unet(Resnet18,34, Inceptionresnetv2のensemble) LB:0.6624

  • Model2:Unet++(EfficientNetB4) LB:0.6639

  • Model1,Model2の出力マスクのnp.logical_or LB:0.6636

出来なかったこと

  • 「BCE + Dice loss 」のような損失関数を+で書いている表記をDiscussionで良く見るのですが、 損失関数を足す際に実装方法や、足すことによるメリットが分からず、実装もできませんでした。
  • →191203追記:Dice lossだけだと、学習時の損失関数の値の減少が安定しない場合、BCEなどを足し合わせることで安定する場合はあるため、らしいです。

  • 過去の上位ソリューションを見ているとFPNがよく登場するのですが、実装の仕方がわかりませんでした。

  • →191203追記:segmentation_models.pytorchの選択肢の一個にありました。GitHub - qubvel/segmentation_models.pytorch: Segmentation models with pretrained backbones. PyTorch.

  • ニューラルネットワークのlayerの自作は、各layerの役割が十分に理解しきれていないため出来ませんでした。

  • →191203追記:論文をもとにレイヤーをちょっといじることが多いそうです。

  • ハイスペックな環境の活用方法が分かりませんでした。V100×8などの環境もPaperspace上に用意されていたのですが、 実際に計算させる場合には、GPUを並列で回すコードの記載が必要らしく、そこで環境使用を諦めてしまいました。

  • Inputの画像サイズをモデル毎に変えている方が多いのですが、そのメリットが理解できませんでした。

  • →191203追記:計算時間の短縮、メモリの節約のためです。

  • Hengさんというケロケロケロッピのアイコンの方(通称ケロッピ先生)が、コンペ終盤で高いスコアが出るコードの共有をして頂き、 明らかにこの時期からLBのスコアの伸びが早くなったのですが、自分は読んでも理解が出来なかったため活かせませんでした・・・ www.kaggle.com

つまり、実装力不足が根本な原因ですね。情けないです。

 

f:id:sinchir0:20191121005513p:plain
情けない顔

反省

  • 時間の見積もりが甘かったです。当初は「他にやるコンペないし、画像の勉強含め雲コンペやるかー」ぐらいの軽い気持ちで始めたのですが、 やっていると段々勝ちたい気持ちが強くなってきてしまいました。1か月でなんとかなると考えた昔の自分を恨みました。

  • もっと落ち着いてコードを読み込めばよかったです。初めて画像系のコードに触れたため、 分からないのは仕方ないにしても、分からないコードを丁寧に調べながら進めればそのうち理解出来たかと思います。 ただ、「なるべく多くSubmitしなければ!」という焦りから、あまり落ち着いて読む時間が取れなかったかなと反省してます。

  • .pyファイルの使い方を学ぶべきと思っています。今までずっとipynbで分析を行っており、 .pyベースでの分析はあまり行ったことがありません。画像系は特にgitからの引用が多いため、 .pyファイルの経験を増やす必要があると感じました。  

    良かったこと

  • 画像コンペに対するハードルが下がりました。

  • 仕事で、画像データを扱うハードルが下がりました。受託分析の仕事をしており、今回の分析はどのデータを使うべきか、という部分から考える機会が結構あります。今までは「画像データ分かりません」でしたが、今は「画像データ少し分かります」になれたため、選択肢が広がったかなと思います。

  • 画像系の話が理解できるようになりました。Twitterで強い方々をフォローしていると、EffiecientNetが~~とかResnetが~~など、僕が理解が出来ないTweetを良く見かけるのですが、最近はそういうTweetが少し分かるようになり嬉しいです。

総論

結構悲惨な結果になってしまったため、とにかく強くなりたいと思いました。