deviation networkの実装とパッケージ化

テーブルデータの異常検知モデルとしてdeviation networkというのがある。
paper: https://arxiv.org/abs/1911.08623
repo: https://github.com/GuansongPang/deviation-network

それまでの精度が高めなdeep learningを活用した異常検知モデルは2ステップによるアプローチが多く、以下のような2段構成になっていた。

  • 入力データから教師なし学習によって特徴量を抽出
  • 抽出された特徴量から異常度合いを表すスコアを算出

異常スコアを計算とそのための特徴抽出を別々に行うために異常を検出するための適切な特徴を表現できていない場合があることや、特徴抽出に教師なし学習を用いることで既知の異常データを利用することができないなどの問題点があった(2019年の論文であることに注意)

この点を解決するため、1ステップで直接異常スコアを計算でき、かつ少数の異常データを事前知識として活用できる弱教師あり学習としたのがdeviation network。

特徴をざっくりまとめると

  • 異常データは正常データに比べて極めて少ないこと仮定
    • 異常データを事前知識として学習してスコアに反映されることが可能
  • 学習
    • 正常データは標準正規分布に、異常データは高スコアとなるようにlossを設定
    • 標準正規分布になるよう学習するためしきい値の設定が容易
      • 信頼区間の考え方が使えてわかりやすい
  • 既存の手法で主である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上の情報を元にとても簡単にできた。

参考