テーブルデータの異常検知モデルとしてdeviation networkというのがある。
paper: https://arxiv.org/abs/1911.08623
repo: https://github.com/GuansongPang/deviation-network
それまでの精度が高めなdeep learningを活用した異常検知モデルは2ステップによるアプローチが多く、以下のような2段構成になっていた。
- 入力データから教師なし学習によって特徴量を抽出
- 抽出された特徴量から異常度合いを表すスコアを算出
異常スコアを計算とそのための特徴抽出を別々に行うために異常を検出するための適切な特徴を表現できていない場合があることや、特徴抽出に教師なし学習を用いることで既知の異常データを利用することができないなどの問題点があった(2019年の論文であることに注意)
この点を解決するため、1ステップで直接異常スコアを計算でき、かつ少数の異常データを事前知識として活用できる弱教師あり学習としたのがdeviation network。
特徴をざっくりまとめると
- 異常データは正常データに比べて極めて少ないこと仮定
- 異常データを事前知識として学習してスコアに反映されることが可能
- 学習
- 既存の手法で主である2ステップな手法と異なり
- データから直接異常スコアを計算するため学習・予測ともに速い
- データの性質をより反映した形で異常スコアを算出できる
公式実装はgithubに上がっており試しに動かしてみるのは簡単にできる。readmeに書いてある通りで動くが以下の部分でデータセット名がハードコードされるので消してから動かすとよい。
https://github.com/GuansongPang/deviation-network/blob/179a74a/devnet.py#L232
また、論文で書かれたデータセットはこちらに置かれている。
https://github.com/GuansongPang/ADRepository-Anomaly-detection-datasets/tree/b32bb0a/numerical%20data/DevNet%20datasets
pytorchによる自前実装
上記1ファイルにほぼすべての処理が入っておりシンプルでわかりやすいものの、個人的によく使うのがpytorchなのと勉強も兼ねてpytorchで実装してみたのがこちら。
https://github.com/y-kamiya/devnet
概要としては
- そもそもシンプルな処理であることと、簡単のためにtrainer.pyとしてほとんどを1ファイルに収めた
- 公式実装で入っていた実験用の処理は除いた
- networkは隠れ層が1層だけのもののみ(論文中でdefaultと書かれたもの)
- ダミーデータの追加によって異常データ数を指定された値に揃える処理は削除
- 入力データが疎行列である場合の対応は削除(公式実装でdata_format=1の場合)
- 学習後の結果表示を追加
こちらのデータに対して実行して公式実装の結果と比較
https://github.com/GuansongPang/ADRepository-Anomaly-detection-datasets/blob/b32bb0a389c9f36136bf9be818d095b68bae45aa/numerical%20data/DevNet%20datasets/annthyroid_21feat_normalised.csv
公式実装の実行結果
annthyroid_21feat_normalised: round 0 Original training size: 5760, No. outliers: 427 5760 427 5333 0 Training data size: 5760, No. outliers: 427 ... $ python devnet.py --data_format 0 --input_path dataset/ --network_depth 2 --runs 1 --epochs 20 --known_outliers 427 --cont_rate 0 --data_set annthyroid_21feat_normalised --ramdn_seed 42 Epoch 19/20 20/20 [==============================] - 0s 20ms/step - loss: 2.0970 Epoch 20/20 20/20 [==============================] - 0s 19ms/step - loss: 2.0551 AUC-ROC: 0.7483, AUC-PR: 0.2501 average AUC-ROC: 0.7483, average AUC-PR: 0.2501
同じdatasetに対する今回の実装による実行結果
$ python src/main.py dataroot=data/debug epochs=20 eval_interval=20 random_seed=42 ... ============================== TableDataset TRAIN data shape: torch.Size([5760, 21]) n_inliner: 5333 n_outliner: 427 ============================== [train] epoch: 0, loss: 2.45, time: 0.09 phase: Phase.EVAL, load data from devnet/data/debug/eval.csv ============================== TableDataset EVAL data shape: torch.Size([1440, 21]) n_inliner: 1333 n_outliner: 107 ============================== ... [train] epoch: 18, loss: 1.98, time: 0.09 [train] epoch: 19, loss: 1.92, time: 0.10 [eval] epoch: -1, AUC-ROC: 0.7452, AUC-PR: 0.2356
seedを指定することで実行のたびに結果が同じになることは確認済み。完全に結果が一致していないものの近い値となった。
また、手元ではkedroと組み合わせて試していたこともあり、packageとして使えた方が便利だったためpypiにも上げてみた。
https://pypi.org/project/devnet/
poetryで管理していたこともあって簡単にpackage化できたというのも理由の一つ。packageを上げるのは今回初めてだったがweb上の情報を元にとても簡単にできた。