UR-DMUモデルを使って異常値を算出する方法
TL;DR
UR-DMU(異常検知モデル)を用いて、映像から異常値を算出する手順を紹介します。
私たちの研究室(NISLab)
アドベントカレンダー 11 日目~
UR-DMUモデルとは
UR-DMUモデルは、弱教師あり動画異常検知のための手法で、次の3つの特徴を持ちます
- GL-MHSA: 動画内の長期・短期的な関係を捉える特徴抽出モジュール
- Dual Memory Units: 正常データと異常データを別々のメモリーバンクに保存し、両者を明確に区別
- NUL: 正常データをガウス分布として学習し、ノイズに強い特徴を生成
これにより、異常検知の精度向上と誤検知の削減を実現します。
詳しく知りたい方は以下の論文を参照してください。
UCF-Crime Dataset
今回はUCF-Crime Datesetというデータセットを使用します。UCF-Crime Datasetは、動画異常検知(Video Anomaly Detection, VAD)のための大規模データセットで、監視映像を中心としたリアルなシナリオを含むデータセットです。
- 規模
- 約1900本の動画で構成されており、監視映像としては非常に大規模
- 13種類の異常イベント(爆発、逮捕、交通事故など)を含む
- ラベル
- トレーニングセット
- 正常な動画:800本
- 異常な動画:810本
- テストセット
- 正常な動画:140本
- 異常な動画:150本
トレーニングセットには動画全体のラベル(正常または異常)が付与され、テストセットにはフレーム単位のラベルが付いています。
3. 用途
弱教師あり(Weakly Supervised)や無教師あり(Unsupervised)の動画異常検知モデルの開発と評価
4. 応用例
監視カメラを用いたセキュリティシステムや、異常行動を検知するAIシステムの研究・開発において活用されています。このデータセットは、異常イベントの多様性や動画ラベルの粒度から、研究者にとって貴重なベンチマークとされています。
このデータセットをUR-DMUモデルで使用するためにnpyファイル形式に変更する必要があります。npyファイル形式のデータは以下からダウンロードできます。今回はモデルを動かしてみることが目的なので、TestフォルダのAbuseのみ扱います。
MacでUR-DMUモデルを動かすための準備
1. UR-DMUモデルのリポジトリをクローン
以下がUR-DMUモデルのリポジトリです。
2. datasetフォルダにデータセットを追加
ルート配下にdatasetフォルダを作成し、先ほどダウンロードしたnpyファイルを追加します。
3. UCF_Test.listの更新
読み込む検証用データセットのパスUCF_Test.listに設定する。
dataset/Abuse/Abuse028_x264__0.npy
dataset/Abuse/Abuse028_x264__1.npy
dataset/Abuse/Abuse028_x264__2.npy
dataset/Abuse/Abuse028_x264__3.npy
dataset/Abuse/Abuse028_x264__4.npy
dataset/Abuse/Abuse028_x264__5.npy
dataset/Abuse/Abuse028_x264__6.npy
dataset/Abuse/Abuse028_x264__7.npy
dataset/Abuse/Abuse028_x264__8.npy
dataset/Abuse/Abuse028_x264__9.npy
dataset/Abuse/Abuse030_x264__0.npy
dataset/Abuse/Abuse030_x264__1.npy
dataset/Abuse/Abuse030_x264__2.npy
dataset/Abuse/Abuse030_x264__3.npy
dataset/Abuse/Abuse030_x264__4.npy
dataset/Abuse/Abuse030_x264__5.npy
dataset/Abuse/Abuse030_x264__6.npy
dataset/Abuse/Abuse030_x264__7.npy
dataset/Abuse/Abuse030_x264__8.npy
dataset/Abuse/Abuse030_x264__9.npy
4. GPUからCPUへの切り替え
GPU非搭載のPCで実行するため、cudaではなくcpuで実行するように変更します。
例えば、ucf_test.pyを実行する際に、以下のtranslayer.py内のモジュールを使用するため、cpuで実行するように設定する必要があります。
class Attention(nn.Module):
def __init__(self, dim, heads = 8, dim_head = 64, dropout = 0.):
super().__init__()
inner_dim = dim_head * heads
project_out = not (heads == 1 and dim_head == dim)
self.heads = heads
self.scale = dim_head ** -0.5
self.attend = nn.Softmax(dim = -1)
self.to_qkv = nn.Linear(dim, inner_dim * 4, bias = False)
self.to_out = nn.Sequential(
nn.Linear(2*inner_dim, dim),
nn.Dropout(dropout)
) if project_out else nn.Identity()
def forward(self, x):
b,n,d=x.size()
qkvt = self.to_qkv(x).chunk(4, dim = -1)
q, k, v, t = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = self.heads), qkvt)
dots = torch.matmul(q, k.transpose(-1, -2)) * self.scale
attn1 = self.attend(dots)
tmp_ones = torch.ones(n).cpu() # cuda() -> cpu()
tmp_n = torch.linspace(1, n, n).cpu() # cuda() -> cpu()
tg_tmp = torch.abs(tmp_n * tmp_ones - tmp_n.view(-1,1))
attn2 = torch.exp(-tg_tmp / torch.exp(torch.tensor(1.)))
attn2 = (attn2 / attn2.sum(-1)).unsqueeze(0).unsqueeze(1).repeat(b,self.heads, 1, 1)
out = torch.cat([torch.matmul(attn1, v),torch.matmul(attn2, t)],dim=-1)
out = rearrange(out, 'b h n d -> b n (h d)')
return self.to_out(out)
UR-DMUモデルによる異常値の出力
1. ucf_test.pyで出力値を取得
ucf_test.pyを以下のように変更する。
import torch
from options import *
from config import *
from model import *
import numpy as np
from dataset_loader import *
from sklearn.metrics import roc_curve, auc, precision_recall_curve
import warnings
warnings.filterwarnings("ignore")
def test(net, config, wind, test_loader, test_info, step, model_file=None):
with torch.no_grad():
net.eval() # モデルを評価モードに設定
net.flag = "Test"
# モデルファイルのロード(CPU用に指定)
if model_file is not None:
net.load_state_dict(torch.load(model_file, map_location=torch.device('cpu'))) # GPUがない場合でもCPUでロード
load_iter = iter(test_loader)
frame_gt = np.load("frame_label/gt-ucf.npy") # Ground Truthをロード
frame_predict = None
cls_label = [] # 正解ラベル
cls_pre = [] # 予測ラベル
temp_predict = torch.zeros((0)).cpu() # 予測結果を格納するテンソル
# テストデータの数を確認
num_test_data = len(test_loader.dataset)
for i in range(num_test_data):
_data, _label, _extra = next(load_iter) # 3つ以上の値を返す場合
_data = _data.cpu() # データをCPUに移動
_label = _label.cpu() # ラベルをCPUに移動
res = net(_data) # モデルの予測結果
a_predict = res["frame"] # フレームごとの予測結果
temp_predict = torch.cat([temp_predict, a_predict], dim=0) # 予測結果を結合
# 10回ごとに評価
if (i + 1) % 10 == 0:
cls_label.append(int(_label)) # 正解ラベルを追加
a_predict = temp_predict.mean(0).cpu().numpy() # 平均予測結果を計算
print(a_predict)
cls_pre.append(1 if a_predict.max() > 0.5 else 0) # 予測ラベルを0か1に変換
# セグメントごとの平均予測値を出力
segment_avg_predict = np.mean(a_predict) # セグメントごとの平均予測値
print(f"Segment {i // 10 + 1} average prediction: {segment_avg_predict:.10f}")
fpre_ = np.repeat(a_predict, 16) # フレームごとの予測結果を複製
if frame_predict is None:
frame_predict = fpre_ # 初回はそのまま代入
else:
frame_predict = np.concatenate([frame_predict, fpre_]) # それ以降は結合
temp_predict = torch.zeros((0)).cpu() # 次回の予測用にテンソルを初期化
# print(frame_predict)
# 使用したテストデータのフレーム数に合わせた評価
fpr, tpr, _ = roc_curve(frame_gt[:len(frame_predict)], frame_predict)
auc_score = auc(fpr, tpr)
# 正解率の計算
correct_num = np.sum(np.array(cls_label) == np.array(cls_pre), axis=0)
accuracy = correct_num / len(cls_pre)
# 精度と再現率の計算
precision, recall, _ = precision_recall_curve(frame_gt[:len(frame_predict)], frame_predict)
ap_score = auc(recall, precision)
# 可視化 (windオブジェクトの使用)
if wind is not None:
wind.plot_lines('roc_auc', auc_score)
wind.plot_lines('accuracy', accuracy)
wind.plot_lines('pr_auc', ap_score)
wind.lines('scores', frame_predict)
wind.lines('roc_curve', tpr, fpr)
# テスト結果を保存
test_info["step"].append(step)
test_info["auc"].append(auc_score)
test_info["ap"].append(ap_score)
test_info["ac"].append(accuracy)
# 結果を表示
print(f"ROC AUC Score: {auc_score}")
print(f"Accuracy: {accuracy}")
print(f"PR AUC Score: {ap_score}")
# テスト関数の実行部分(例)
if __name__ == "__main__":
# 引数の解析
args = parse_args()
# 設定ファイルの読み込み
config = Config(args)
# モデルの初期化
net = WSAD(input_size=config.len_feature, flag="Test", a_nums=60, n_nums=60)
# CPU用に設定
device = torch.device('cpu') # GPUがなくてもCPUに設定
net = net.cpu() # モデルをCPUに移動
# テストデータのローダー準備
test_loader = data.DataLoader(
UCF_crime(root_dir=config.root_dir, mode='Test', modal=config.modal, num_segments=config.num_segments, len_feature=config.len_feature),
batch_size=1, shuffle=False, num_workers=config.num_workers
)
# テスト情報を保持する辞書
test_info = {
"step": [],
"auc": [],
"ap": [],
"ac": []
}
# モデルファイルのパスを指定してテスト関数を呼び出す
model_file = os.path.join(args.model_path, "ucf_trans_2022.pkl")
test(net, config, None, test_loader, test_info, step=0, model_file=model_file)
変更点
-
モデルのロード方法
- モデルファイルをロードする際に map_location=torch.device('cpu') を指定し、CPU環境でも動作するように変更しています。
net.load_state_dict(torch.load(model_file, map_location=torch.device('cpu')))
-
GPUからCPUへの切り替え
- cpu() に変更して、すべてのテンソルをCPUで処理するように修正しています。
_data = _data.cpu() _label = _label.cpu()
-
入力データの変更
- next(load_iter) が3つ以上の値(例: _data, _label, _extra)を返すことを考慮しています。
_data, _label, _extra = next(load_iter) # 3つの値を受け取る
-
セグメント予測の詳細表示
- 各セグメントの平均予測値を計算し、表示するロジックを追加しています。
segment_avg_predict = np.mean(a_predict) # 平均予測値 print(f"Segment {i // 10 + 1} average prediction:{segment_avg_predict:.10f}")
-
テストデータの制限
- 使用するテストデータのフレーム数を制限してROC曲線を計算する際に考慮しています。
fpr, tpr, _ = roc_curve(frame_gt[:len(frame_predict)], frame_predict)
-
可視化処理(wind オブジェクト)
- windオブジェクトがNoneでない場合のみ可視化を実行するように条件分岐を追加しています。
if wind is not None: wind.plot_lines('roc_auc', auc_score) wind.plot_lines('accuracy', accuracy) wind.plot_lines('pr_auc', ap_score) wind.lines('scores', frame_predict) wind.lines('roc_curve', tpr, fpr)
-
結果の表示
- AUCスコアや精度などを標準出力に表示する処理を追加しています。
print(f"ROC AUC Score: {auc_score}") print(f"Accuracy: {accuracy}") print(f"PR AUC Score: {ap_score}")
-
テストデータセットの統計情報
- テストデータセットのデータ数を計算し、使用しているデータ数を明示的に表示しています。
num_test_data = len(test_loader.dataset)
-
実行環境への対応
- torch.device('cpu') を利用して、環境に依存しない形に修正しています。
device = torch.device('cpu') net = net.cpu()
2. 対象データ名の表示
どの検証用データに対する異常値であるか明確にするために、dataset_loader.pyの内容を以下のように変更します。
class UCF_crime(data.DataLoader):
def __init__(self, root_dir, modal, mode, num_segments, len_feature, seed=-1, is_normal=None):
if seed >= 0:
utils.set_seed(seed)
self.mode = mode
self.modal = modal
self.num_segments = num_segments
self.len_feature = len_feature
split_path = os.path.join('list','UCF_{}.list'.format(self.mode))
split_file = open(split_path, 'r')
self.vid_list = []
for line in split_file:
self.vid_list.append(line.split())
split_file.close()
if self.mode == "Train":
if is_normal is True:
self.vid_list = self.vid_list[8100:]
elif is_normal is False:
self.vid_list = self.vid_list[:8100]
else:
assert (is_normal == None)
print("Please sure is_normal=[True/False]")
self.vid_list=[]
self.prev_name = None
def __len__(self):
return len(self.vid_list)
def __getitem__(self, index):
if self.mode == "Test":
data,label,name = self.get_data(index)
if name != self.prev_name: # 変更点: 検証用データ名を表示
print(f"Processed segment: {name}")
self.prev_name = name
return data,label,name
else:
data,label = self.get_data(index)
return data,label
def get_data(self, index):
vid_info = self.vid_list[index][0]
name = vid_info.split("/")[-1].split("_x264")[0]
video_feature = np.load(vid_info).astype(np.float32)
if "Normal" in vid_info.split("/")[-1]:
label = 0
else:
label = 1
if self.mode == "Train":
new_feat = np.zeros((self.num_segments, video_feature.shape[1])).astype(np.float32)
r = np.linspace(0, len(video_feature), self.num_segments + 1, dtype = np.int)
for i in range(self.num_segments):
if r[i] != r[i+1]:
new_feat[i,:] = np.mean(video_feature[r[i]:r[i+1],:], 0)
else:
new_feat[i:i+1,:] = video_feature[r[i]:r[i]+1,:]
video_feature = new_feat
if self.mode == "Test":
return video_feature, label, name
else:
return video_feature, label
3. コンソールから異常値を確認
ucf_test.pyを実行すると、コンソールに検証用データごとの異常値と全体のAUCやAccuracyなどが出力されます。
Processed segment: Abuse028
[0.83644617 0.90005285 0.90124434 0.9061573 0.92932904 0.9611279
0.9806412 0.99022883 0.9905113 0.98253614 0.9427327 0.91359484
0.9034573 0.9015921 0.90115577 0.90082467 0.9004857 0.90029144
0.90023386 0.9002455 0.8999394 0.896904 0.8163482 0.8013464
0.799609 0.64263135 0.6040614 0.6009702 0.60041213 0.6003483
0.60047036 0.6008206 0.6008647 0.60084593 0.60092866 0.6024629
0.61771387 0.69661677 0.6999687 0.699998 0.7000001 0.7000001
0.699998 0.69999135 0.69998205 0.6999734 0.6999529 0.69954526
0.6550769 0.61550254 0.6051635 0.6028514 0.602292 0.6040448
0.6117225 0.6555503 0.7024754 0.72649825 0.77899194 0.76806986
0.7174956 0.7015345 0.6327425 0.60123587 0.5314406 0.504593
0.500822 0.5002512 0.5001005 0.5000436 0.49997988 0.49977812
0.49740037 0.4701318 0.44319233 0.42363578 0.40621462 0.39198923
0.38194627 0.37307513 0.3661762 0.36214694 0.3599601 0.3609868
0.36481676 0.37182742 0.38212687 0.39613968]
Segment 1 average prediction: 0.6658592820
Processed segment: Abuse030
[4.99973059e-01 5.00290573e-01 5.01084924e-01 5.00233293e-01
4.98018175e-01 3.46814185e-01 2.69604325e-01 1.07132033e-01
9.44673270e-02 3.42662297e-02 2.32234057e-02 2.87023820e-02
7.36263096e-02 9.55639929e-02 7.55357295e-02 2.48029623e-02
9.38605703e-03 4.21333825e-03 1.66240486e-03 9.51788214e-04
4.11845976e-04 5.67802519e-04 1.65562110e-03 7.25614047e-03
8.02475289e-02 2.17941314e-01 2.55728096e-01 2.73321062e-01
2.31787279e-01 1.91369176e-01 2.33993568e-02 2.20034365e-03
5.13359206e-04 1.85582932e-04 9.75370131e-05 6.70148438e-05
5.30054895e-05 4.72434476e-05 4.10220455e-05 3.80263009e-05
3.60443410e-05 3.82442777e-05 4.06396721e-05 4.61875316e-05
5.07154036e-05 5.18345514e-05 5.43383940e-05 6.07739275e-05
6.50021684e-05 7.63355129e-05 8.83341418e-05 9.23980697e-05
1.00033838e-04 1.15327326e-04 1.33573543e-04 1.38388656e-04
1.36959614e-04 1.46358259e-04 1.53478773e-04 1.50182765e-04
1.32029905e-04 1.36377319e-04 1.29458233e-04 1.17740325e-04
1.11911701e-04 1.23441059e-04 2.21629540e-04 8.83684203e-04
1.10535901e-02 1.81148767e-01 2.34254166e-01 3.39843929e-01
6.04726493e-01 6.42099261e-01 7.76701629e-01 8.05133641e-01
8.23536396e-01 7.54877210e-01 5.52282631e-01 5.31592429e-01
5.67786157e-01 6.36863053e-01 6.99045062e-01 7.02403486e-01
7.05301285e-01 7.16068387e-01 7.84917951e-01 8.44330907e-01
9.39471543e-01 9.99758124e-01 9.99973655e-01 9.99956429e-01
9.99192417e-01 5.72758794e-01 3.10381830e-01 8.18915963e-02]
Segment 2 average prediction: 0.2374735624
ROC AUC Score: 0.8110632183908046
Accuracy: 1.0
PR AUC Score: 0.13859977763187512
最後に
UR-DMUモデルを使って異常値を出力する方法を紹介しました。今回は一部の検証データのみで実装したため、是非全ての検証データで実装してみてください。また、ROC曲線のプロットも実装していないので、windオブジェクトを設定して実装しでみてください。ハイパーパラメータなども再設定することができるので、様々なパターンで試してみると、より高い精度が得られるかもしれません。
参考文献
Discussion