LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

異常を発見せよ!サーバレス×AIで挑む異常検知の未来

Last updated at Posted at 2023-11-06

はじめに

異常検知は大量のデータから通常とは異なるものを検出する技術です。異常検知には様々な手法が応用されており、近年ではAIや機械学習モデルを活用した新たな手法が次々と登場しています。

IoT.kyotoでも時系列データに焦点を当てていくつかの機械学習モデルを検証してきました。

  • k近傍法(ブログはこちら)
  • K-means法(ブログはこちら)
  • Random Cut Forest(ブログはこちら)

今回、Random Cut Forestを使った異常検知デモを作成しましたのでその中身についてご紹介したいと思います。デモについてはこちらのブログで公開していますので併せてご覧ください。

アルゴリズムの詳細についてはここでは言及しませんので興味のある方はGuha et al. (2016)をご参照ください。

Random Cut Forestとは

データセット内の異常なデータポイントや外れ値を検出できる機械学習アルゴリズムです。Random Forestアルゴリズムをベースとした教師なし学習であり、各データポイントで異常スコアを算出します。

算出された異常スコアの値が高いほどそのデータポイントは異常である可能性が高いと考えますのでこのスコアを使って異常を定義します。

Pythonでの実行

Pythonで実行するにはrrcfライブラリを使用します。公開されているサンプルコードを参考にしながら下図のデータについてスコア算出してみます。

Figure_1.png

算出したスコアを$z$軸にプロットしたものが下図です。

output.gif

$y$-$z$軸で切り取って見てみると下図のようになります。

スクリーンショット 2023-11-26 13.51.35.png

異常データとして手動追加したデータポイントのスコアが高くなっていますね。それだけでなく正常データについても中央付近と端っこ付近のデータの一部でもスコアが高いことがわかりますので今回のデータセットではこの辺りが偽陽性になりやすそうだということもわかります。

アーキテクチャ

ここからは今回作成したデモの詳細について触れていきます。まずは今回作成したアーキテクチャの紹介です。シンプルなサーバレスアーキテクチャを使っていて可視化画面からデータを取得する際のLambda上でRandom Cut Forestを実行しています。

time_series_demo-ページ1.drawio.png

なお、DynamoDBに保存されているデータはIoT.kyoto CurrenTIAで取得した温度データを10秒間隔でSORACOM Funnel、AWS IoT Core経由でAmazon DynamoDBへ保存したものを使用しています。

currentia-pattern-1024x374.png

なぜLambdaで実行するのか

AWS上でRandom Cut Forestを利用する場合、Amazon SageMakerAmazon Kinesis Data Analyticsを実行環境にすることが多いと思いますが、いずれもランニングコストが高いことがネックでした。サーバレスなアーキテクチャで低コストで手軽に異常検知が実現できないかと考えて今回のアーキテクチャを採用しました。

実装

ここからは実装の詳細について触れていきます。

先述したrrcfですが今回はLambda上で実行するのですがデフォルトではLambdaにインストールされていないのでLambda Layerを使用する必要があります。

Lambdaの中で実施している処理フローは以下です。

  1. DynamoDBからデータ取得(説明省略)
  2. データの整形
  3. forestの作成(rrcf使用)
  4. スコア計算(rrcf使用)
  5. 閾値の計算
  6. 各データポイントの異常判定

ここからは各処理についてポイントを絞って説明していきます。

2. データの整形

DynamoDBから取得したデータのうちrrcfで使用するデータを抽出・加工してnumpyへ変換しています。後続の処理がnumpy前提のためnumpyへの変換は必須だと思います。

data = []
for item in data_res:
    data.append([float(item["COL_NAME"])])
X = np.array(data)

3. treeの作成 〜 4. スコア計算

ここがメインの処理です。何も考えずにサンプルコードをそのまま実行すると実行時間に問題があることがわかりました。

python サンプルコード(Batch anomaly detection)抜粋.py
num_trees = 100
tree_size = 256
sample_size_range = (n // tree_size, tree_size)

# Construct forest
forest = []
while len(forest) < num_trees:
    # Select random subsets of points uniformly
    ixs = np.random.choice(n, size=sample_size_range,
                           replace=False)
    # Add sampled trees to forest
    trees = [rrcf.RCTree(X[ix], index_labels=ix)
             for ix in ixs]
    forest.extend(trees)

処理するデータ件数が処理時間にどの程度影響するのかLambda(デフォルトスペック)の実行時間を調べました。なお、tree_sizeは$n <= 20$の時は$n$, $20 < n < 256$ の時は$n/3$としています。

データ件数 データ取得時間 Tree作成時間 スコア計算時間 合計 異常検出可否
10 0.2114 1.5947 2.1598 3.9660 ×
50 0.2497 2.3025 2.1209 4.6730 ×
100 0.2554 4.8409 2.7411 7.8374
250 0.3688 9.5814 3.7209 13.6712
500 0.4867 9.9379 3.6185 14.0431
1000 0.8231 24.9798 5.1596 30.9625
2500 1.7249 26.7606 5.4385 33.9240

データ件数が100件程度あれば単純なスパイクは異常判定できそうでしたが、データ更新、画面更新のたびに8秒かかるので正直使えないです・・・

今回は1次元データに対する異常検知であり、処理するデータ件数もそこまで多くないので下記部分は精度に影響を及ぼさないのでは?と思いました。

# Select random subsets of points uniformly
ixs = np.random.choice(n, size=sample_size_range,
                       replace=False)

そこでnum_trees=1, tree_size=データ件数として再度検証してみると実行時間、精度ともにデフォルトのパラメータ値よりも良くなりました。

データ件数 データ取得時間 Tree作成時間 スコア計算時間 合計 異常検出可否
10 0.2653 0.0754 0.0001 0.3407 ×
50 0.3111 0.1016 0.0003 0.4130
100 0.3248 0.1809 0.0005 0.5062
250 0.3775 0.3370 0.0013 0.7158
500 0.5662 0.3598 0.0223 0.9483
1000 0.8994 0.5601 0.1193 1.5788
2500 1.7748 0.7571 0.2228 2.7547

データ件数が大きいケースでは既存のパラメータ値は実行時間、精度ともにバランスがよいが、データ件数が極端に少ないケースや次元が低いケース、異常が単純なケースでは精度向上への寄与度は高くない、あるいは逆効果の可能性があるのではないかと思います。

追加検証をする場合はハイパーパラメータチューニングが必要になるのでひとまず今回はnum_trees=1, tree_size=データ件数として実装することで実行時間の問題をクリアしました。

5. 閾値の計算

最初は「3シグマのルール」を採用して平均値$+ 3 \times $標準偏差を閾値に設定していました。しかし、偽陽性が多かったので調整を重ねて最終的に今回は平均値$+ 5 \times $標準偏差の値を閾値として設定しました。

# 5σの計算
avg_codisp_arr = np.array(avg_codisp)
avg_codisp_arr_mean = np.mean(avg_codisp_arr)
avg_codisp_arr_std = np.std(avg_codisp_arr)
threshold = avg_codisp_arr_mean + avg_codisp_arr_std * 5

閾値の設定は難しく、明確な答えがないのが現状かなと思います。データが正規分布に従うケースでは99.7%以上のデータが平均値$\pm 3 \times$標準偏差の範囲に収まりますので「3シグマのルール」を採用するとうまくいくことが多いかと思います。

しかし、正規分布に従わないケースではこれは正しくありません。分布の仮定を大きく緩めたチェビシェフの不等式(下式)では88.8%ですので9割未満となります。もし同不等式で95%以上のカバー率を出すには$5σ(k=5)$が必要になります。

\mathbb{P}\left(|X-\mu|\leq k\sigma \right) \geq 1- \frac{1}{k^2}

6. 各データポイントの異常判定

各データポイント毎に異常判定を実施してフロントへデータを返す処理です。

# スコア結果の整形
res = []
for i in range(LIMIT):
    anomaly_score = avg_codisp_arr[i]
    if anomaly_score > threshold:
        anomaly_result = True
    else:
        anomaly_result = False
    res.append(
        {
            "device_id": data_res[i]["device_id"],
            "COL_NAME": X[i][0],
            "anomaly_score": avg_codisp_arr[i],
            "anomaly_result": anomaly_result,
        }
    )

最後に

今回はRandom Cut Forestを使って作成した異常検知デモの中身についてご紹介させていただきました。

低コストなサーバレス環境で異常検知を実装するためにはまだまだ課題があります。時系列データの周期性を考慮した異常検知の実現を目先の課題として引き続き研究を進めていきたいと思います。

参考文献

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up