これは何?
某社で行われている社内コンペのメモです。私の備忘録でもあります。
結果:2位
精度:68.3%
120枚をクラス1~3で各40枚としていました。
ただし、その数値に合わせに行こうとすることはしませんでした。
今回の目的変数
「画像が何に種別されるか」の3値分類問題です。
回答は、
・通常細胞
・変性細胞
・変性細胞(悪化型)
とよくある形です。
今回の評価指標・ベースレート
Accuracy(精度)です。
出題者によると、お試しの意味も込めてAccuracyだそうです。
普通なら複合指標による評価が妥当とされています。
Precision(適合率:真の陽性をいかに漏れなく見分けられたか)やAUC(認識性能の高低を表す)、MAP(Mean Average Precision、Precisionの応用)などなど。
ベースレートはaccuracy60%です。
僕は、検証の際はAUCを用いていました。
他のコンペでよくある、「指標はRMSE、ただMAEで評価を行ったほうが結果が良くなる」という事例を途中で採用しました。
今回の説明変数の特徴
画像
訓練に使用できるのは1,200画像
極度に少ないです。
通常10万~とある場合が多いのですが、今回はこの制約下で行います。
ただし、カラー写真ですので染色等での色の変化(非常に重要)は確認できます。
なぜ色の変化が必要かというと、見た限りでは核を染色しているし、その他にも染色処置をしていたためです。
変性細胞には核の形態変化が確認でき、その形態変化をDNN(Deep Neural Network)が捉えるには色が必要ではないか?ということです。
※本来は詳細情報を当該領域の研究者(顧客の会社所属)に確認したかったのですが…不可能だったことが残念です。
※このようなドメイン知識は作成する上で必要になります。
見分けるのには訓練が必要
画像を「変性細胞である」とは見てわかるのですが、「悪化している」かはわかりませんでした。
* 一応生化学系の修士 → 製薬会社(感染症系)に居たのである程度はわかりましたが…他の人はわからないそうです。
悪化判断ために、本を実家から取り寄せ&知り合いのおすすめを新規購入しました。
結局、変性細胞の種類が多すぎて判断できなかった…。
※特に細胞診はどの部位であるかが重要です。そもそもの細胞の形状が違います。今回はそのラベリングは存在せず、3クラスラベルしかなかったので、「大丈夫かコレ?」とはなっていました。
分け方が雑
画像を確認したところ、採取部位別にデータがまとまっているわけではないようでした。
なぜなら通常細胞の形がそもそも違うとひと目でわかりました。
一般的に組織ごとに細胞の形は異なりますし、それらが変化した変性細胞も見た目が異なります。細胞への症状としては、核の膨張や(免疫機構の攻撃による)細胞の生死が確認できました。
問題としては画像の中心に変性細胞がなかったり、写っている細胞数がごく少なかったり、同じ細胞を回転させただけで撮っている可能性がある写真も確認できました。
今回の目標
コピペしない。
画像認識の良い解決法を見つける。
ある程度個人で古典的な画像認識から深層学習を学んでいたので、どんな手法があるかなどは少し分かる状態でした。
Kerasなどの説明書をメインに、解説書(Kaggle等のDiscussion含む)、論文を見てモデルを構築します。
今回行った作業の流れ
画像を見て、前処理を考える
当たり前ですが、最初に画像を確認しました。
難しいポイントは何かアタリを付けましょう。
今回はフィルタ等は使用しませんでした。
※ノイズを取り除くにはいろいろなフィルタ(ガウシアン、バイラテラル、プリューウィット、LoG等)があります。
listにopen_cvで読んだ画像データを入れて、以下のような処理を行いました。
def image_preprosessed(img): # ノイズ除去 # https://qiita.com/icoxfog417/items/53e61496ad980c41a08e # https://www.slideshare.net/takahirokubo7792/ss-71453093 #img = cv2.GaussianBlur(img, (1,1), 0) return img
※実際は処理を行っていない。
自作Sequentialモデルを作成
よくあるConvを重ねただけの単純なモデルを作成しました。(精度30%)
モデルの定義
def model_base(model): model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(image_scale, image_scale, 3))) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(64, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activation='relu')) model.add(layers.GlobalAveragePooling2D()) model.add(layers.Dropout(0.5)) return model
Dense層を置いていないのは、GlobalAveragePooling(GAP)のほうが、
・精度が良く
・パラメータが少なくなるので計算が早い
という性質があるからです。(詳細:https://arxiv.org/pdf/1312.4400.pdf)
GAPからGlobalMaxPoolingの置換も効果ありそう(精度+0.5%)ということは分かりました。
モデルの実装
model_no1 = models.Sequential() model_no1 = model_base(model_no1) model_no1.add(layers.Dense(n_categories, activation='softmax')) model_no1.compile( loss='categorical_crossentropy', optimizer=optimizers.Adam(lr=1e-4), metrics=['categorical_accuracy'] )
追加でmerticsにkerasのAUCを導入していました。
optimizer(最適化関数)も結局はAdamではなく、SGDでした。
最適化関数のSGDは学習は遅くなるものの、Adamのように局所解に落ち込みにくいので、結果精度が高くなる傾向にあります。
DNNは(数学的に見ると)学習曲面上で局所解が多いらしい(詳細は理解できなかった…)ので、精度を上げるためには問題に合った最適な最適化関数を選択する必要があります。
学習済みモデルのファインチューニング(1)
よくあるVGG16をblock5_conv1層付近でファインチューニングしました。(精度50%)
モデルの定義
base_VGG16 = VGG16(weights='imagenet', include_top=False, input_shape=(image_scale, image_scale, 3))
学習層の定義
base_VGG16.trainable = True set_trainable = False for layer in base_VGG16.layers: if layer.name == 'block5_conv1': # この層以降は学習可能 set_trainable = True if set_trainable: layer.trainable = True else: layer.trainable = False
書いてある通り、転移学習はある層以降の学習を可能にして行うことが多いです。
前半の層はさまざまな種類のフィルタ構造になっており(確認するには各層を可視化するとOK)、画像の各特徴量(角、線、円等)を自動的に抽出する働きがあります。
後半の層は、事前学習用のモデルに合わせた重みになっているので、適用したい問題に合わせた重みに変化させます。
そのため、どの層から学習させるかにも検討すること必要です。
調べている限り、Batch Normalizationのみ学習可能にさせるなど、色々方法がありそうです。
とはいえ、VGG16は全く検証ができなかったことが残念です。
元々の精度が低かったので放置しました。
学習済みモデルのファインチューニング(2)
Xceptionを108層以降ファインチューニングしました。
モデルの定義
base_XP = Xception(weights='imagenet', include_top=False, input_shape=(image_scale, image_scale, 3))
※include_topをTrueにすると、元々の画像サイズにTrain画像を合わせないと動かなくなります。なお、それぞれのモデルで画像サイズが違う問題があるので、input_shapeで設定しています。
学習層の定義
base_XP.trainable = True for i, layer in enumerate(base_XP.layers): if i <= 108: # BNだけTrueにする if layer.name.startswith('batch_normalization') or layer.name.endswith('bn'): layer.trainable = True print("◆%s\r\n %s is Trainable:%s" % (i, layer.name, layer.trainable)) else: layer.trainable = False print("◆%s\r\n %s is Trainable:%s" % (i, layer.name, layer.trainable)) else: layer.trainable = True print("◆%s\r\n %s is Trainable:%s" % (i, layer.name, layer.trainable))
Trainすべき層が問題に合ってるのかどうか甚だ疑問です。
ただ、こちらで一番上手く行ったので、ここに記載させていただきました。
学習済みモデルのファインチューニング(3)
InceptionResNetV2をblock8_10_conv層以降ファインチューニングしました。
Kerasで導入可能なモデルは、https://keras.io/ja/applications/を参考にしてください。
モデルの定義
base_IRN2 = InceptionResNetV2(weights='imagenet', include_top=False, input_shape=(image_scale, image_scale, 3))
学習層の定義
base_IRN2.trainable = True set_trainable = False for layer in base_IRN2.layers: if layer.name == 'block8_10_conv': # 要検討 set_trainable = True if set_trainable: layer.trainable = True else: layer.trainable = False
結果としては、Xceptionとほぼ同等でした。
ただし、このモデルだけPseudo Labelingを行うと精度が下がる問題を抱えていました。
評価指標の変化を確認
ここまでで4モデルを作成しました。
・自作簡易モデル
・VGG16ファインチューニング
・Xceptionファインチューニング
・InceptionResNetV2(IRNV2)ファインチューニング
それぞれ、TrainのLoss(損失関数)とAccuracy(精度)、AUCを、Testからどれくらい離れていたかを確認しました。
K-Foldクロスバリデーション(K=5~10)も試しましたが、学習・検証に時間かかりすぎたので途中でホールドアウト法に戻しました。(良くないことです)
ホールドアウト法でも問題ないデータ量(n>10,000)なら良いのですが…
基本的にファインチューニングを行うようなモデルは表現力が高い(意訳:色々と細かいところまで学習できる)がゆえに過学習しやすいので、Testと回答の精度が離れがちです。
そのため、どのモデルがTestで見える見た目の精度と回答の精度が近いか確認しておきます。
・結果
基本、XceptionとIRNV2は精度も高く適正な値を保ちやすかったです。DropOutを高い値(0.7付近)に持っていたことも要因と考えています。
VGG16は過学習しすぎで、自作簡易モデルはデータに対してモデルが小さすぎて学習できず…という結果でした。
検討事項
・予測に使うDense層の前にDropOut層の強さ(0.01~0.99)を検討すること
→ 特徴量がうまく生成できない場合、非常に高い(0.9)値で上手く行ったことがある。(らしい)
・画像数の水増し(Data Augmentation)
→ ImageGeneratorを使っていたので変更は簡単でしたが、意味を理解して使えてない気がしました。ここも要検討です。
# 機能追加 # https://towardsdatascience.com/image-augmentation-for-deep-learning-using-keras-and-histogram-equalization-9329f6ae5085 datagen_train = ImageDataGenerator( rescale=1./255, rotation_range=90, # 回転 width_shift_range=0.9, # 横シフト height_shift_range=0.9, # 縦シフト zoom_range=0.4, horizontal_flip=True, vertical_flip=True, fill_mode='wrap', # abcd|abcd|abcd )
→ ImageGeneratorを使う場合、メモリが足りなくなる時があります。その時はbatch_sizeを減らすと良いです。
※計算時間はかかるようになりますが、計算は止まらなくなります。
history = model_xp.fit_generator( datagen_train.flow(X_train, y_train, batch_size=10), epochs=100, validation_data=datagen_valid.flow(X_test, y_test, batch_size=10), steps_per_epoch=100, callbacks=[model_decay, model_check] )
・LearningRateSchedulerの変更タイミング
→ 基本100epochsで学習を行っていたので、学習率を変更する(下げていく)タイミングをどうするか決めかねて適当に設定していました。
# 学習epochごとに学習率低減を実装(all100) def step_decay(epoch): x = 1e-3 if epoch >= 80: x = 1e-4 if epoch >= 83: x = 1e-5 if epoch >= 86: x = 1e-6 if epoch >= 89: x = 1e-7 if epoch >= 92: x = 1e-8 if epoch >= 95: x = 1e-9 return x
初期を1e-3にしている理由は、あまりにも学習率を上げると学習済みモデルの重みを参考にせず、壊してしまうことが起こるためです。
少しずつ計算を進め、最適解を探すようにしましょう。
・ModelCheckpointの設定
→ Test_AUC基準で取得していました。
・画像を大きくすると精度は上がるのか?(フルカラーの写真のため)
→ 少し大きくすると精度は上がりました。さらに大きくするとデータ量が爆発してメモリに乗らない…
出来なかったことは以下の通りです。
・モデル内での回答の重み付け
→ 変性細胞(悪化型)の識別精度が悪いこともあり、「変性細胞(悪化型)の回答の確率出力をさらに重み付けるような方法に変更してはどうか」ということを考えていました。
重み付けアンサンブルからの着想ですが、うまくいくのかどうかわかりません。
・アンサンブル
→ 結局XceptionとIRNV2の差が大きすぎたので、アンサンブルしても意味がありませんでした。
画像の前処理を変更したモデル同士でのアンサンブルや、Pseudo LabelingしたXception+しないIRNV2でのアンサンブル等も考えられます。
・ガウス過程でモデルの適正さ?を計算し、アンサンブルの重み付けを最適化できるらしい(未確認)
・輝度勾配ヒストグラムを局所特徴量として扱うことができる(普通画素のみだが)らしい(未確認)
注意すべきポイント
適切に評価できる評価体形を作らない限り、改善が実際に進んでいるか全然わからなくなります。
基本的に最新のモデルになればなるほど最終的な精度は高くなりやすい傾向にあります。
最新のモデルは下記で検索可能です。
State of The Art…Image Classification on CIFAR-100
レビュー論文と言って、論文をまとめた論文があるのでそちらで検索も非常に早いです。
下記は2017年時点のレビュー論文です。
Deep Convolutional Neural Networks for Image Classification: A Comprehensive Review
英語が苦手でも大丈夫。
ページごとGoogle翻訳するか、PDFはGoogleDriveに保存してDocumentで開くとテキスト化できます。
その他:画像水増し
下記のように画像に加工を加える方法があるので、それぞれ試します。
・ノイズを加える
・平滑化する(ガウシアンフィルタ)
・明度・彩度の調整
・グレースケール化
・ImageDataGeneratorの改良
→ とはいえ、CNNのフィルタ構造と同じような感じで調整すると意味がないので、その点は気を付けます。
1,200画像データから適切に学習するのは非常に難しいので、いろいろな角度から画像を学習できるようにします。
その他:画像水増し対応モデルの作成
上記に対応できるモデルを作成します。
グレースケール化であるとRGB3色から白黒1色になるため、一般的なモデルのファインチューニングは困難になる・・・のか?
(RGBからグレースケールへの計算式は存在するので変換は可能か?)
最後に
一位になりたかった
・・・いろいろと試せ、理解が進んだので良かったです。
やってみたではなく、何を考えたか、なぜそう考えたかを包括的に書いた記事が少ないので、少しずつですがアップしていこうと思います。
その他コードやデータサイエンス関連の質問はR:Twitterまでお願いします。