アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

kerasでLSTM(QRNN)を使って異常検知手法で急上昇ワードをやってみた(機械学習-変化点検知)

急上昇ワード(バースト検知)とえば、googleyahoo!でも似たようのがある。今回はあれほど高性能じゃないけど、急上昇ワードと同じ仕組みのものを異常検知手法でやってみた。

目次
・訓練
閾値の設定
・テスト
・実際に急上昇ワードをやってみる

今回はBigQuery(BQ)から抜き出した、一定期間内の検索ワードの急上昇を検知してみる。みんな大好きニューラルネットワークのLSTMで訓練し、バースト検知に利用した。訓練から、実際にBQから取り出した検索ワードの急上昇検知実行までの流れを一通り書いて行く。




訓練


まず訓練データ、「mizugi」と「bts」というワードを2つ、8ヶ月分ほど抽出。btsは2桁(10台)の検索数が多く、mizugiは検索数が多く、3桁(100〜1000)のワードだ。
まずLSTMでbtsを8ヶ月分くらい、急上昇部分があっても構わず学習させた。


btsとmizugiの急上昇部分は特異変換スペクトルで検出した。結果がこれ

bts
f:id:trafalbad:20180826171931p:plain

mizugi
f:id:trafalbad:20180826171944p:plain

アルゴリズムはLSTMを使った。QRNNとかいう発展版も試したが、LSTMの方がkerasで簡単に使えるし、十分なので途中で断念。

データをLSTMで使えるようにする正規化とかの前処理と、モデル作成はこんな感じ

def get_data(data, time_steps):
    docX, docY = [], []
    for i in range(len(data)-time_steps):
        docX.append(data[i:i+time_steps])
        docY.append(data[i+time_steps])
    alsX = np.array(docX)
    alsY = np.array(docY)
    return alsX, alsY

def transform_data(data, inverse_option, scaler):
    data_shape = data.shape
    if inverse_option is True:
        data = scaler.inverse_transform(data)
    else:
        data = scaler.fit_transform(data)
    data = data.reshape(data_shape)
    return data, scaler

def prepare_data(original_data, time_steps):
    copy_data = original_data.copy()
    scaler = MinMaxScaler(feature_range=(0, 1), copy=False)
    data, scaler = transform_data(data=copy_data, 
                              inverse_option=False, scaler=scaler)
    
    x,y = get_data(data, time_steps=time_steps)
    return x, y, scaler

time_steps = 12

x, y, scaler = prepare_data(X_train, time_steps)

input_dim=x.shape[-1]
timesteps=x.shape[1]

model = Sequential()
model.add(LSTM(300, input_shape=(timesteps, input_dim),
         stateful=False,return_sequences=True))
model.add(Flatten())
model.add(Dense(1, kernel_initializer='lecun_uniform'))
model.add(Activation("linear"))
# model.load_weights('/Users/d/model.ep200.h5')
model.compile(loss="mean_squared_error", optimizer="adam",)

以下が訓練後、btsの学習期間を予測した図。赤がリアルの検索数値で青が予測値だ。btsは2桁の値なのか知らないけど、損失関数はあまり減少しなかった。学習結果としてはいまいちだったけど、それなりによく予測できてたかな。

btsの予測
f:id:trafalbad:20180826172015p:plain


閾値の設定


ここからbtsとmizugiから以下のように検証・テスト1・テスト2の3つにデータを分けた

・検証データ→btsの急上昇のない部分(異常なしデータ)

・テストデータ1→btsの急上昇のある部分(異常ありデータ)

・テストデータ2→mizugiの急上昇のある部分(異常ありデータ)


バースト検知は異常検知の中で「変化点検出」に当たる。

それには少し変わった閾値設定をする必要がある。以下の流れで閾値を設定した。

①検証データはオリジナルの閾値の設定に使用

②検知したいワードの平均値をオリジナルの閾値にかけて、その検知したいワードに最適な閾値を再設定する


変化点検知の閾値設定には、回帰(予測)モデルと、mseの組み合わせから求めるのが、結構メジャーらしい(参考記事)。




①検証データはオリジナルの閾値の設定に使用

今回の閾値の設定には、検証データを使って最初の(オリジナルの)閾値を求めた。コードは以下の通り。

def calculate_mse(value, predict_value, variance=1.0):
    value = value[:, 0]
    predict_value = predict_value[:, 0]
    mse_value = [(v - p_v)**2 for v, p_v in zip(value, predict_value)]
    return np.array(mse_value)


mse_value_valid = calculate_mse(x_scale_valid, predict_valid)
threshold = np.max(mse_value_valid)
# threshold:249.16269761799867

検証データ(異常なしデータ)の全期間から、MSEを求めて、その最大値を閾値とした(つまり、検証データの異常値の最大値)。




②検知したいワードの平均値をオリジナルの閾値にかけて、その検知したいワードに最適な閾値を再設定する


ここで問題なのは。btsは2桁の値なので閾値はかなり低い。これを水着のような3桁、4桁以上の検索数のワードに適用すると閾値が小さすぎて、全部の異常値を検出してしまう。


そこで検知するワードに対して最適な閾値を再設定してやる必要がある。以下のやり方で設定した。


1.検証データから求めたMSEの最大値をオリジナルの閾値とする

2.検知したいワードの全期間の検索数の平均値割る2の値を四捨五入して整数にする

3.それをオリジナルの閾値にかけて、検知したいワードの閾値(high_threshold)として再設定

わかりにくいので、コードで表すとこんな感じ

mse_value_normal = calculate_mse(x_scale_train, predict_train)
N=int(np.mean(x_scale_train)/2)
high_threshold = threshold*N 

なんで2で割ったのかは、いろいろ試して一番検出に適した値になるからなので、別に整数(int)じゃなくて、小数(float)のままでもよかった。





テスト


再設定した閾値で、テストデータ1、2のバースト(異常)を上手く検知できてるか試す。

上からテストデータ1(bts)、テストデータ2(mizugi)の時系列データと、再設定した閾値をプロットした

bts
f:id:trafalbad:20180826172037p:plain

mizugi
f:id:trafalbad:20180826172050p:plain

元の閾値よりも高い閾値で、程よく急上昇部分が検知されてる。一日、1週間、一月の期間でも試したが、上手く検出できてた。





実際に急上昇ワードをやってみる


いよいよ、バースト検知を試してみる。今回はjupyter上から直接、検索ワードをBigQueryから抽出して、検出してみた。


急上昇検知のメイン部分はこんな感じ

csvs=[]
original_threshold = 270.2200 
train_data_mean=4.964
num_words=len(words)
for i in range(num_words):
    cnt=pd.DataFrame(list(cnt_listed[i][0])).dropna()
    hour=pd.DataFrame(list(hour_listed[i][0])).dropna()
    hours=np.array(hour[window:])
    wo=cnt_listed[i][1] # each word
    
    x_predict, y_ ,scaler = prepare_data(cnt, time_steps)
    predicted, x_scale, mse_value = predict_model_show_graph(hour[window:], x_predict, y_, scaler, model, time_steps)
    mse_value = calculate_mse(x_scale, predicted)
    
    N=int(np.mean(x_scale)/2)
    high_threshold = original_threshold*N  # 閾値
    if high_threshold<original_threshold:
        #print('high_threshold: {}'.format(original_threshold))
        #show_graph_threshold(hour[window:], mse_value, original_threshold, 'Anomaly Score test', "r")
        # 急上昇ワードの時間を表示
        for mse, hour in zip(mse_value, hours):
            if mse >=original_threshold:
                csvs.append([hour, wo])
    else:
        #print('high_threshold: {}'.format(high_threshold))
        #show_graph_threshold(hour[window:], mse_value, high_threshold, 'Anomaly Score test', "r")
        # 急上昇ワードの時間を表示
        for mse, hour in zip(mse_value, hours):
            if mse >=high_threshold:
                csvs.append([hour, wo])

その前の前処理では、取り出したワードを時系列データに変換したり、LSTM用に正規化したりした。

今回は3月3日の分だけ、検索ワードを取り出して、検知。図をplotとするといい具合に検出できてるっぽい。

最終的には、ワードと時間を抽出する形にした。


閾値の設定は前に書いた動的時間伸縮法(DTW)でもできるけど、計算量がバカにならない。なので、MSEとかのローコストな計算式を臨機応変に使い分けるのが最適だと思う




今回はLSTMを用いた、変化点検出手法で急上昇ワードのバースト検知をやってみた。
昔はchange finderとかsmartfilterとか、ゴツい検出アルゴリズムがあったけど、ニューラルネットワークの登場で異常検知もかなりやりやすく、精度も高くなった。

異常行動検知もLSTMを使ってできる。機械学習シリーズにある密度比推定とか非構造学習みたなガチもんの専門的な内容には、機会があったら触れてみたい。

追記:QRNNで可視化してみた
LSTMより精度のいいらしいQRNNで同様に、訓練・可視化してみた。この参考サイトではQRNNのコードの一部を修正してるが、2019/2月時点ではgithubのはもう修正済みだったので、修正の必要なかった。

# git clone してpythonファイルをコピー
$ git clone https://github.com/DingKe/nn_playground && cd nn_playground/qrnn 

$ cp /Users/desktop/nn_playground/qrnn/qrnn.py /Users/desktop/qrnn.py
from qrnn import QRNN

#省略
print(x.shape) # (6354, 12, 1)
print(y.shape) # (6354, 1)

"""
モデルの作成
自身の状態をリセットするかしないかを指定するstateful(周期的な波形はTrueの方が若干良い)
"""
input_layer = Input(shape=(12, 1))
qrnn_output_layer = QRNN(64, window_size=12, dropout=0)(input_layer)
prediction_result = Dense(1)(qrnn_output_layer)
model = Model(input=input_layer, output=prediction_result)
model.compile(loss="mean_squared_error", optimizer="Adam")

# 訓練( batch_size:指定しなくてもいい)
history = model.fit(x, y, epochs=200, batch_size=500)

btsの予測 by LSTM
f:id:trafalbad:20180826172015p:plain


btsの予測 by QRNN
f:id:trafalbad:20190223144258p:plain


LSTMの予測結果と比べると若干いいのでは?くらいだと思う。条件が単純すぎるから、もっと未来を予測とか複雑な条件であれば、はっきり精度の差は出ると思う。