Kaggleの画像コンペに初めて挑んでみた
はじめに
Kaggleの雲コンペ「Understanding Clouds from Satellite Images」に参加し見事惨敗したため、ここに反省と備忘録を残します。
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から削除
メダル圏外の悲しい結果でした
shake down _(:3」∠)_ pic.twitter.com/z7TruDi0c3
— しんちろ (@sinchir0) 2019年11月19日画像コンペの基本的な部分は理解出来たため、次からは良い成績を残したいです。
分析環境
画像コンペは、画像処理に伴う並列計算が多く、GPUが必須となります。 GCPやAWS上で環境構築する方が多いかなと思いますが、個人的にはPaperspaceがお勧めです。
クラウドに自分だけのコンピュータを!Paperspaceを使ってみた!www.fusenstage.com
pytorch設定済のGPU環境が、ボタンポチポチすれば数分で構築出来て、
非常に簡単です。
ただし、JupyterNotebookを使う場合は、定期的にstorage配下以外のデータが削除されるため注意が必要です。
(一度、storage以外でデータ管理を行い全データ削除の憂き目にあっています…) また、計算時間が短い場合はGoogleColaboratoryを用いて計算していました。 colab.research.google.com
コンペ概要
自分で書こうかなと思っていたのですが、とても分かりやすい記事が投稿されていたため紹介します。
簡単に言うと、雲の形が4種類のどれか( Sugar(砂糖), Flower(花), Fish(魚), Gravel(砂利))を当てるSegmentationのタスクです。
Segmentationとは
僕がこのレベルからのスタートだったため、簡単に説明します。 画像コンペには大きく3つのタスクが存在します。
Classification
Detection
Segmentation
下記ブログが分かりやすかったため、画像をお借りして説明します。 starpentagon.net
Classification
該当の画像が何なのかを予測します。
上記の例だと、出力はbirdtoyとなります。
Detection
該当の画像の中で、対象物がどこにあるのかを矩形範囲で予測します。
出力は上記画像の矩形範囲となります。
Segmentation
該当の画像の中で、対象物がどこにあるのかをピクセル単位で予測します。
出力は上記画像から、元の画像を除いたもの(マスクと呼びます)となります。
今回はSegmentationのコンペであるため、提出ファイルは各画像に対するマスクのファイルとなります。
前半2週間の過ごし方
初めての画像コンペへの取り組みだったため、本当にDiscussion読んでもEDA読んでも何言ってるのかさっぱり分からんって感じでした。
そのため、わけのわからん単語を一つ一つ検索したり、実際に自分でコードを回して挙動の確認を繰り返しました。
その時期に、kernel含め、ひたすら参考にしたWebサイトを下記に貼ります。
「kernel」
EDAは下記のkernelを中心に読みました。非常に分かりやすい。
「Keras Documentation」
NN系ライブラリ、Kerasの公式のF&Qです。ここでの学びが一番多かった気がします。
例えば、モデルのセーブの仕方、ロードの仕方はここで知りました。
from keras.models import load_model #モデルのセーブ model.save('my_model.h5') #モデルのロード model = load_model('my_model.h5')
「albumentations」
DataAugmentationのライブラリである albumentationsの公式サイトです。
DataAugmentationとは、学習データの画像を縦に回転させたり、輝度を変えたりして
データを増やす手法です。
下の例はDataAugmentationの一つ、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をベースに取り組みました。
Unetとは
一言でいうと、Segmetationが得意なネットワークです。 ちゃんとした理解は出来ていませんが上記ぐらいの認識で最初はいいんじゃないかと 自分に言い聞かせてごまかしています。
下記記事が分かりやすかったです。
このUnetをベースに下記のようなことに取り組んでいました。
Backboneの変更
Backboneとは、Unetのネットワークの一部であり、入力画像の特徴を抽出する役割を持つ部分です。 下記記事の概念が分かりやすかったため紹介させて頂きます。
下記画像の1.Backboneの部分が該当します。
この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
と予測結果を平均します。一種のアンサンブルかと思います。
下記ブログの画像が分かりやすかったため紹介します。
PostProcessing
PostProcessingは「後処理」という意味です。モデルを学習させ出力した予測結果に対し、 更に行う処理のことを表します。
僕の場合は、各画像をClassificationして、どの画像か( Sugar(砂糖), Flower(花), Fish(魚), Gravel(砂利))の閾値を出力し、 その閾値を下回ったマスクを削除するPostProcessingを行っていました。各kernelをベースにさせて頂きました。
アンサンブル
単純に、各ピクセルごとの予測結果の確率の平均を取りました。 アンサンブルは下記のような感じです。
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
つまり、実装力不足が根本な原因ですね。情けないです。
反省
時間の見積もりが甘かったです。当初は「他にやるコンペないし、画像の勉強含め雲コンペやるかー」ぐらいの軽い気持ちで始めたのですが、 やっていると段々勝ちたい気持ちが強くなってきてしまいました。1か月でなんとかなると考えた昔の自分を恨みました。
もっと落ち着いてコードを読み込めばよかったです。初めて画像系のコードに触れたため、 分からないのは仕方ないにしても、分からないコードを丁寧に調べながら進めればそのうち理解出来たかと思います。 ただ、「なるべく多くSubmitしなければ!」という焦りから、あまり落ち着いて読む時間が取れなかったかなと反省してます。
.pyファイルの使い方を学ぶべきと思っています。今までずっとipynbで分析を行っており、 .pyベースでの分析はあまり行ったことがありません。画像系は特にgitからの引用が多いため、 .pyファイルの経験を増やす必要があると感じました。
良かったこと
画像コンペに対するハードルが下がりました。
仕事で、画像データを扱うハードルが下がりました。受託分析の仕事をしており、今回の分析はどのデータを使うべきか、という部分から考える機会が結構あります。今までは「画像データ分かりません」でしたが、今は「画像データ少し分かります」になれたため、選択肢が広がったかなと思います。
画像系の話が理解できるようになりました。Twitterで強い方々をフォローしていると、EffiecientNetが~~とかResnetが~~など、僕が理解が出来ないTweetを良く見かけるのですが、最近はそういうTweetが少し分かるようになり嬉しいです。
総論
結構悲惨な結果になってしまったため、とにかく強くなりたいと思いました。