こんにちは. changです.
前回紹介したU-Netによる複数種類の異常検知*1ですが,Class_2の検知が出来ていないと思ってみていたらViewerに間違いがありました. これを含めて,特徴や大きさの異なる複数の異常を検知する場合の課題について考察してみようと思います.
0. 回転不変性
間違っていたのはViewerからネットワークに画像を入力するところです. 90度回転した画像をネットワークに入れていました(汗).
C#/Viewer/Form1.cs(修正後)
private void btnInferAll_Click(object sender, EventArgs e) ... arrIn[CHANNEL*(i * IMG_SIZE + j) + n] = (resized.GetPixel(i, j).R + resized.GetPixel(i, j).G + resized.GetPixel(i, j).B) / 3.0f;
C#/Viewer/Form1.cs(修正後)
private void btnInferAll_Click(object sender, EventArgs e) ... arrIn[CHANNEL*(i * IMG_SIZE + j) + n] = (resized.GetPixel(j, i).R + resized.GetPixel(j, i).G + resized.GetPixel(j, i).B) / 3.0f;
逆に言うと,Class_2以外では画像が90度回転していても異常検知できていたことになります. ここで考えたいのが,ニューラルネットワークの回転不変性です.
畳み込みニューラルネットワーク(CNN)の特徴の一つに移動不変性があります. 画像の中の何処に異常が写っていても良い,という事です. 一方で,回転や縮小についてはそこまでロバストでは無いというのが一般論です*2.
DAGM画像は乱数を使って計算機が生成した画像(だと思う)ので,基本的に,バックグラウンドや異常はランダムに散らばっています. 特定の方向に画像が偏っていることが無く,回転によって画像の特徴が大きく変わることは少ないと言えます. この事が,画像を90度回転させる誤ったソースでも,そこそこの異常検知を実現できた原因と考えられます.
Class_1の90度回転
画像全体がランダムなので,回転させても印象が変わらない
しかし,Class_2の画像を良く見ると,多くの画像が上下方向に流れる川の中に,横方向に傷が入った構図になっています. 90度回転すると,全く違った印象になります. だから,90度回転させると異常検知できなかったのです.
class_2の90度回転
上下方向に川が流れ,横方向に傷が在る.画像を回転させると大きく印象が変わる
この結果から,やはりディープ・ラーニングに回転不変性は無いことが解りました. 正にミスからの学びですね.
1. 大きさの異なる異常の検知
ディープ・ラーニングに複数種類の異常をまとめて学習させる場合,どうしても面積の小さい異常の方が見つかり難くなります. DAGMではClass_2やClass_3がシビアになります. ですので,kerasを使い始める前は,異常の面積でラベルを正規化していました*3. このやり方でtensorflowでは動いていたのですが,kerasのbinary_crossentropyでは学習できなくなりました. kerasでは入力画像を-1.0~1.0のレンジに補正する必要があるのですが,そのレンジに対してラベルが凄く小さな値(0.001)になる所為だと思います. そこで,1.0に近い値で重みを付けてみました.
Class | 重み |
---|---|
1 | 0.9 |
2 | 1.0 |
3 | 0.9 |
4 | 0.9 |
5 | 0.9 |
6 | 0.8 |
ラベルの重み付け.
異常が小さいClass_2を1.0で強調し,異常が大きいClass_6を0.8で軽視する.
ソースでは下記の様に記述しています. 学習データの配置に時間が掛かるので,今回からConfigurate_dataをC言語で書き替えました.
configurate_data.cpp
vector<float> weight = {0.9, 1.0, 0.9, 0.9, 0.9, 0.8}; ... labelAll[(j*IMG_SIZE + i)*CATEGORY + n + 1] = weight[n]; // defection
ラベルが浮動小数になるので,float型で定義する様に変えました. 以前に,kerasのbirany_crossentropyがbool型のラベルしか受け付けないと書いてしまいましたが,間違いでした.
load_data.py
def readLabels(self, filename, DATA_SIZE): images = np.zeros((DATA_SIZE, self.IMG_SIZE, self.IMG_SIZE, self.channels), dtype=np.float)
ラベルに合わせてViewer側も少し変える必要があります. 今回,例えばClass_1ではラベルを0.9にしていますが,これをやると推論の出力も0.9(付近)になります. ですので,異常の閾値にも重みが反映されるようにします.
viewer.cs
private void showInferenceAll(float[] arrOut) { Bitmap detected = (Bitmap)resized.Clone(); for (int i = 0; i < IMG_SIZE; i++) { for (int j = 0; j < IMG_SIZE; j++) { // class 1 if (arrOut[CHANNEL * (i * IMG_SIZE + j) + 0] > 0.8) // ←ここ { detected.SetPixel(j, i, Color.Red); } // class 2 if (arrOut[CHANNEL * (i * IMG_SIZE + j) + 1] > 0.9) // ←ここ { detected.SetPixel(j, i, Color.Yellow); }
Class_2を検出するようになりました.
ただですね,,,実は画像回転のバグを治せば,重みラベルを使わなくてもClass_2もClass_3も検出されるんです!!!. 何でかな... 以前から気になっているですが,kerasでは出力画像が1.0~0.0の範囲に自動調整されますよね. これが,Batch Normalizationの様な効果を生んでいるのではないかと予想しています. 異常の小さな画像が多く含まれたバッチを正規化すれば,重み付けと同じ効果になると思うので...
2. むすび
float型のラベルを使える事が判ったのは収穫だったんですが,いまいち,すっきりしない考察になってしまいました.
ソースは,アップ済みを更新しています*4.