【異常検知】深層距離学習で画像の歪みを検出する

ディープラーニングを使った画像の異常検知について、研究が盛んに行われています。

工業製品を対象に、正常データのみを使って異常品を検出するアプローチとしては、
今のところ、@daisukelab　さんの[自己教師あり学習](https://qiita.com/daisukelab/items/e0ff429bd58b2befbb1b)が一番有力です。こちらの研究では、
課題として、一部の画像で精度が伸び悩み、異常クラスのデータの作り方を**試行錯誤した**
とのコメントがありました。

そこで、本稿では、**多様な**異常画像の自動生成方法について模索してみたいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/361d7bdb-a03a-b38f-e3a7-a876a7e25f8c.png)

※こちらは[Pythonデータ分析勉強会＃16](https://pythondata.connpass.com/event/161835/)の発表資料です。
※コード全体は[こちら](https://github.com/shinmura0/AutoEncoder_vs_MetricLearning/blob/master/AnomalyDetection_self_supervised.ipynb)に置きました。

#結論から
+ 先行研究の[自己教師あり学習](https://qiita.com/daisukelab/items/e0ff429bd58b2befbb1b)では、「正常」、「線を描画した異常」の2クラスに対して、本稿では「歪みを加えた異常」を追加して**3クラスの深層距離学習**（下図参照）で学習させた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/17a5e00a-6740-bbd8-f2fa-fe737a311b22.png)
+ 3クラスになったことにより、深層距離学習の最新手法「[AdaCos](https://qiita.com/shinmura0/items/d2a3a46c3fb287a1ea0e)」の使用も可能となった。
+ 本手法は、一部データでは精度が良くなったものの、悪くなるデータもあり、**諸刃の剣**となった。


#画像の考察
[先行研究](https://qiita.com/daisukelab/items/e0ff429bd58b2befbb1b)は、非常にシンプルな方法でありながら、多くの欠陥を捉えることに成功しています。
さらに、@daisukelab さんのfasi.aiのコードは、非常にすっきりしていて可読性も高いです。
画像の異常検知に興味がある方は、是非ご一読されることをお勧めます。

先行研究では、[MVtech AD](https://www.mvtec.com/company/research/datasets/mvtec-ad/)のScrew、Transistor、Grid、Pillの4つのデータでAUCが0.9を下回る
結果となり、試行錯誤をしたとの内容でした。

今回はPill以外のデータを対象にしたいと思います。
Pill以外の3つのデータは以下の通りです。

![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/58d60783-77e1-bdf5-b975-a46e45a2926e.png)

異常画像を見ると、傷や汚れというより、**「歪み」を生じている**ものが多く、この検出が
難しいため、AUCが伸びにくいものと推察されます。

そこで、正常データから**「歪みデータ」を自動的に生成し**、異常検知性能を押し上げる方法を模索します。

#「歪みデータ」の自動生成
歪みデータを作るには、色々な手法があると思われますが、本稿では画像の一部を回転する方法を
採用しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/06be584c-b87d-404c-55eb-bf38c451513a.png)

これはKerasのDataGeneratorを使えば簡単に実行可能です。

```python
from keras.preprocessing.image import ImageDataGenerator
    small_fig = fig[begin1:end1,begin2:end2]

    datagen = ImageDataGenerator(
               rotation_range=30,
               width_shift_range=0.2,
              height_shift_range=0.2,
              horizontal_flip=False,
              vertical_flip=False)

    for d in datagen.flow(np.expand_dims(small_fig, axis=0), batch_size=1):
        # datagen.flowは無限ループするためループを抜ける
        break

    fig[begin1:end1,begin2:end2] = d[0]
```

##意味のある場所に適用
ただし、**背景画像**の一部を回転させても意味はありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/2a644b53-5d04-babf-45bb-cdbfbe80d508.png)

そこで対象物を自動検知するアルゴリズムを実装しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/3c870d82-1ff2-e0cb-aed4-88ebee60f1dc.png)

Screwなどでは、学習データにおいて試験片の方向や位置が多少ズレています。
それらに対し、学習データを平均化すると試験片が消えたような画像が得られます
（2.Training data average）。

その後、平均化した画像とターゲットとなる画像の差分をとり、二値化（Binary map）
することで試験片の範囲を自動検知することができます。自動検知を実行しながら、
画像の一部を回転させると、以下のような画像が得られます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/0edc9eeb-a07a-1924-ef71-adc747b90014.png)

歪んでいるような画像が得られました。

#本手法の手順
+ 正常画像を用意する。
+ 正常画像に細い線を描き、「線を描いた異常」クラスの画像を生成する。（実験では、OpenCVを用いて、下図のような画像を生成）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/e5883080-bdb9-c0f8-13f3-767cf0e77ac0.png)
+ 提案手法を正常画像に適用し、「歪みを加えた異常」クラスの画像を生成する。
+ 用意した3クラス（下図参照）の分類問題を深層距離学習で学習させる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/17a5e00a-6740-bbd8-f2fa-fe737a311b22.png)
+ 学習後、LOFやコサイン類似度で異常度を算出する。

#実験
[MVtech AD](https://www.mvtec.com/company/research/datasets/mvtec-ad/)を使って実験を行いました。
コード全体は[こちら](https://github.com/shinmura0/AutoEncoder_vs_MetricLearning/blob/master/AnomalyDetection_self_supervised.ipynb)に置きました。Colabで実行可能です。

##条件
+ 深層距離学習は「L2-SoftmaxLoss」、「ArcFace」、「[AdaCos](https://qiita.com/shinmura0/items/d2a3a46c3fb287a1ea0e)」（[クラス数が3以上](https://qiita.com/shinmura0/items/d2a3a46c3fb287a1ea0e#%E5%88%B6%E7%B4%84)になったことにより**適用可能に！**）を使用。
+ 「L2-SoftmaxLoss」はLOFにて異常度を算出
+ 「ArcFace」と「AdaCos」は全ての学習（正常）データに対し、コサイン類似度を計算し、一番大きい値を採用する（「L2-SoftmaxLoss」「ArcFace」については、[こちら](https://qiita.com/shinmura0/items/06d81c72601c7578c6d3#2019%E5%B9%B4metric-learning%E8%B7%9D%E9%9B%A2%E5%AD%A6%E7%BF%92%E3%81%AB%E3%82%88%E3%82%8B%E7%95%B0%E5%B8%B8%E6%A4%9C%E7%9F%A5)の記事を参考にしてください）
+ epochは10、最適化手法はAdam
+ バッチサイズは32
+ ベースモデルはMobileNet V2（学習済モデルを使用、つまり転移学習）
+ 異常クラス（線、歪み）はバッチ毎にランダムに描画するため、学習に使う関数はKerasのtrain_on_batchを使用
+ 10回試行してAUCを算出
+ データはScrew、Transistor、Gridを使用

##AUCの結果
実験結果では、class2（「正常」と「線を描いた異常」の2クラス）がベースライン、
class3（class2のクラス+「歪みを加えた異常」）が提案手法になります。

+ Screw

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/23b9f29f-365c-6415-7bb6-14cf53cb156a.png)

Screwでは、class2に対し、提案手法（class3）を適用するとAUCが低下して
しまいました。低下した理由は後で、考察で検討します。

+ Grid

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/6406f050-d5db-c8b9-53db-3364cbddf89c.png)

Gridでは、期待とおり、ベースライン（class2）よりも、提案手法（class3）の方が
AUCが上昇しました。狙い通り、歪みが検出できたかと思います。異常部分が検出
できているかは可視化で検証します。

+ Transistor

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/e26499d9-6442-3f41-d35e-a75c06e03fc5.png)

Transistorでは、Gridと同様に提案手法（class3）を適用するとAUCが上昇しました。
ArcFaceに至っては0.1以上の向上が確認できます。

##可視化の結果
[先行研究](https://qiita.com/daisukelab/items/e0ff429bd58b2befbb1b)にあるように、Grad-CAMによる異常部分の可視化も行ってみました。
ただし、「AdaCos」をベースにしています。そのため、異常クラスが二つあり、
それぞれのクラスでGrad-CAMを実行しています。

まずは、Gridのデータです。

![ダウンロード (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/7098c38b-e365-efc5-fe9a-8cafc6b70578.png)

「歪みによる異常（Distortion）」の可視化が全てのデータに対し、うまく適合しています。
ただし、異常が複数ある場合、一個しか検出できない場合もあるようです。

次にTransistorです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/ffba5d5b-8483-6a9e-7114-a73ac2c30278.png)

Transistorでは、「線による異常（Slight line）」の可視化がほとんどのデータに対し、うまく適合しています。
Gridとは反対の結果になりました。ここら辺はどちらのクラスの異常を見れば良いのか、データの種類によって
変わるかもしれません。

#実験（番外編）
最後に、ベースモデルの変更（MobileNetV2→EfficentNetB0）を行ってみます。

##条件
先の実験とほとんど同じ条件です。違う点は以下のとおりです。

+ ベースモデルは[EfficentNetB0](https://github.com/titu1994/keras-efficientnets)（学習済モデルを使用、つまり転移学習）
+ 5回試行してAUCを算出

##AUCの結果
※中央値が見えづらいですが、後で表にまとめて記載しております。

+ Screw

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/103cb3b7-003e-4807-df5d-9e332aa7d8c8.png)

Screwでは、class2であってもAUCが0.5を下回る結果に。

+ Grid

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/23e3d085-d644-02cc-fe4e-fcc6463b566e.png)

Gridでは、MobileNetV2と同じような結果に。
やはり、**class3にした方が良いスコアが出ます。**

+ Transistor

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/044887d5-4b62-e4e2-5ca6-f74ddbdcbfe6.png)

Transistorでも、MobileNetV2と同じような結果に。
やはり、class3にした方が良いスコアが出ます。

#実験結果のまとめ
ここまでの結果を中央値で比較してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/0e8bcdaa-ee5d-c887-f71d-86a0fdbf86ca.png)

Screwを除くと、ベースライン（class2）に対し、**提案手法（class3）を適用するとAUCの上昇**が
確認できます。また、距離学習の種類については、どれもそんなに差がついているようには
見えませんが、ArcFaceが割と良いスコアを示しています。

CNNモデルによるAUCの差は、AUC0.9超えを達成しているEfficientNetB0が良さそうに見えます。
EfficientNetB7とかを適用すると、より顕著にAUCが上昇するかもしれません（ただし、画像の
最小サイズは600×600らしいです。重い。ちなみに、B0は224×224。）

#考察（なぜScrewのAUCは低下したのか？）
ScrewのAUC低下の原因を考えてみます。

端的にいって、「歪みを加えた異常」クラスを追加することにより**「大きな」歪みは異常として
検出できる**ようになったが、**「小さな」歪みは逆に正常に近いものとして**認識されるように
なったと思われます。

先行研究の2クラス分類では、細い線を描くことにより、傷や変色はもちろん、Screw先端に
線を描くことで「小さな」歪みを疑似的に再現することができました。ところが、3クラス目を
導入することにより、CNNのタスクがより難しくなります。

今まで、2クラス分類では、「小さな」歪みは、どちらかというと細い線のクラスだった
のに対し、3クラス分類になったことにより、**CNNの特量抽出が線検出と回転検出に特化し、**
「小さな」歪みは正常クラスに近くなってしまったと思われます。その結果、AUCの低下を
招いたものと推察されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264781/dc273216-42c1-c299-4ab1-b64bf1aa8772.png)

上の図はScrewで学習させたAdaCos(class3)にGrad-CAMを適用した図です。画像は全て
異常画像です。ご覧のとおり、Screw先端の小さな歪みは全然検出できていないことが分かります。

さらに、MobileNetV2→EfficientNetB0に変更すると、class2であってもAUCが0.5を
下回る結果になりました。これはEfficientNetB0の方が、より特徴を明確に抽出できるため
「細い線」の特徴抽出がうまくいき過ぎる結果となり、「小さな」歪みは正常に近づいて
しまったものと思われます。

では、一部回転させる範囲を小さくすれば「小さな」歪みを疑似的に再現できるのではないか？
という疑問が生まれますが、範囲を小さくし過ぎるとCNNでは検出できず、学習が全然進まなく
なります。

これを改善するには画像の解像度を上げたり、あるいは試験品の方向を揃えるなど
画像に特化した工夫が必要になります。[先行研究](https://qiita.com/daisukelab/items/e0ff429bd58b2befbb1b)でも言及されているように、
異常データが入手可能であれば、その画像をしっかり見て分析してみることも、
有効だと思われます。

おススメの手順としては、まずは、自己教師あり学習で2クラス分類。その後、
3クラス分類を試してAUCが悪化するようなら、画像をしっかり分析するという
プロセスになります。

#まとめ
+ 「歪みを加えた異常」クラスを追加して、3クラスの深層距離学習+自己教師あり学習で学習を行った。
+ TransistorやGridでは、**AUCの上昇（最大でAUC：0.75→0.87）が確認できた。**一方、Screwでは、**逆にAUCの低下を招いた。**
+ そのため、本手法を適用するかどうかは、**データを見て慎重に判断しなければならない。**
+ Pillは歪みというよりは変色異常の画像が多いため、本手法は適さないため試していない。
+ 深層距離学習の種類については、ほぼ同等の精度。ただ、「AdaCos」はパラメータチューニングが不要なため非常におススメ。