基於自編碼器實現無監督異常檢測系統

作爲自編碼器的入門項目,我實現了一個無監督的異常檢測系統,傳統的異常檢測手段有很多,在有監督時可以單純用多分類問題來判別異常,也可以用高斯聚類來幫助判別異常出現的概率。這裏如果要實現無監督學習的異常檢測,一種方法是藉助數據降維和聚類來幫助我們實現無監督數據聚類,然後在此之上進行異常檢測。
這裏用MNIST的1-9的數字作爲正常樣本,0的數字作爲異常樣本。使用Auto-encoder降維和一些聚類算法。最後評估降維後的正常樣本和異常樣本在特徵空間上的某些區別,找到一個合適的超參數閾值區分正常樣本和異常樣本。

準備工作

首先導入Pytorch等工具包

import torch
import torchvision
import torch.nn as nn
from torchvision import transforms
from torchvision.utils import save_image
from torch.autograd import Variable
import torch.utils.data as Data
import numpy as np
import warnings
warnings.filterwarnings('ignore')
import tensorflow as tf

在tensorflow裏把MNIST直接導入內存並預處理以備使用

(x_train_origin,t_train_origin),(x_test_origin,t_test_origin) = tf.keras.datasets.mnist.load_data()
X_train = x_train_origin/255.
X_test = x_test_origin/255.

X_normal = [X_train[i] for i in range(len(X_train)) if t_train_origin[i]!=0]
y_normal = np.ones(len(X_normal))
X_anomaly = [X_train[i] for i in range(len(X_train)) if t_train_origin[i]==0]

X_normal = torch.tensor(X_normal)
X_anomaly = torch.tensor(X_anomaly)
y_normal = torch.tensor(y_normal)
X_normal = X_normal.float()
X_anomaly = X_anomaly.float()
y_normal = y_normal.float()

train_data = Data.TensorDataset(X_normal,y_normal)

定義一些需要用到的超參數

# 超參數
EPOCH = 10
BATCH_SIZE = 64
LR = 0.005

train_loader = torch.utils.data.DataLoader(dataset=train_data,
                                          batch_size=BATCH_SIZE, 
                                          shuffle=True)

定義Auto-encoder,這個模型實現的是自動進行數據的壓縮和重建,本質上是逼近一個數據分佈;如果我們用正常樣本訓練模型,模型就會在異常樣本處出現較低的概率。

模型搭建與訓練

class AutoEncoder(nn.Module):
    def __init__(self):
        super(AutoEncoder, self).__init__()

        # 壓縮
        self.encoder = nn.Sequential(
            nn.Linear(28*28, 500),
            nn.Tanh(),
            nn.Linear(500, 125),
            nn.Tanh(),
            nn.Linear(125, 40),
            nn.Tanh(),
            nn.Linear(40, 5),
        )
        # 解壓
        self.decoder = nn.Sequential(
            nn.Linear(5, 40),
            nn.Tanh(),
            nn.Linear(40, 125),
            nn.Tanh(),
            nn.Linear(125, 500),
            nn.Tanh(),
            nn.Linear(500, 28*28),
            nn.Sigmoid(),       # 激勵函數讓輸出值在 (0, 1)
        )

    def forward(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return encoded, decoded

autoencoder = AutoEncoder()

訓練,模型的輸入和輸出都是同樣的樣本

optimizer = torch.optim.Adam(autoencoder.parameters(), lr=LR)
loss_func = nn.MSELoss()

for epoch in range(EPOCH):
    for step, (x,y) in enumerate(train_loader):
        b_x = Variable(x.view(-1, 28*28))   # batch x, shape (batch, 28*28)
        b_y = Variable(x.view(-1, 28*28))   # batch y, shape (batch, 28*28)
 
        encoded, decoded = autoencoder(b_x)
 
        loss = loss_func(decoded, b_y)      # mean square error
        optimizer.zero_grad()               # clear gradients for this training step
        loss.backward()                     # backpropagation, compute gradients
        optimizer.step()                    # apply gradients

數據將在AE的幫助下降低到一個更容易處理的維度。

問題分析

我們先嚐試用降維後的數據分佈直接訓練另一個模型,可以直接訓練一個高斯混合模型,但這裏我使用的是一個K-means的聚類模型。

origin_data = Variable(train_loader.dataset.tensors[0].view(-1, 28*28).type(torch.FloatTensor))
encoded_data, _ = autoencoder(origin_data)    # 提取壓縮的特徵值
X = encoded_data.detach().numpy()
from sklearn.cluster import KMeans

clustering_model = KMeans(n_clusters=9, random_state=9)
clustering_model.fit(X)
centers = clustering_model.cluster_centers_

沒有比較好的評估方式,我們直接用樣本點和距離最近的簇中心的距離作爲評判標準,並觀察異常樣本是不是擁有比較特殊的分佈,以至於它離各簇都比較遠。

centers = clustering_model.cluster_centers_

#計算樣本與簇中心的距離
Dist2cluster = []

for center in centers:
    Dist2cluster.append(np.sum((X-center)**2,axis = 1))
Dist2cluster = np.array(Dist2cluster)
Dist2cluster = np.min(Dist2cluster, axis = 0)

import seaborn as sns
#把異常的數據拿出來看一看簇中心距
origin_data = Variable(X_anomaly.view(-1, 28*28).type(torch.FloatTensor))
encoded_data, recon_data = autoencoder(origin_data)
X_anomaly = encoded_data.detach().numpy()

Dist2cluster_anomaly = []
for center in centers:
    Dist2cluster_anomaly.append(np.sum((X_anomaly-center)**2,axis = 1))
Dist2cluster_anomaly = np.array(Dist2cluster_anomaly)
Dist2cluster_anomaly = np.min(Dist2cluster_anomaly, axis = 0)

plt.figure(figsize = (10,4))
plt.subplot(121)
plt.title('Normal')
sns.kdeplot(Dist2cluster,shade=True)
plt.subplot(122)
plt.title('Anomaly')
sns.kdeplot(Dist2cluster_anomaly,shade=True)

在這裏插入圖片描述
大概是因爲簇與簇直接本就存在差異,異常樣本和正常樣本的差異並不容易從K-means中看出。我們可以藉助另一種判別標準,我們認爲貼合數據分佈的圖片通過AE重構後,更不容易失真。因此,我們可以考慮從重構誤差下手,計算原圖片與重構圖片的誤差值(MSE)。

def Reconstruct_error(model, data):
    input_data = Variable(data.view(-1, 28*28).type(torch.FloatTensor))
    _, output_data = autoencoder(input_data)
    input_data, output_data = input_data.detach().numpy(), output_data.detach().numpy()
    #二值化
    
    MSE = np.sum((output_data-input_data)**2,axis = 1)
    return MSE

err_normal = Reconstruct_error(autoencoder, train_loader.dataset.tensors[0])
err_anomaly = Reconstruct_error(autoencoder, origin_data)

plt.figure(figsize = (10,4))
plt.subplot(121)
plt.title('Normal')
sns.kdeplot(err_normal,shade=True)
plt.subplot(122)
plt.title('Anomaly')
sns.kdeplot(err_anomaly,shade=True)

在這裏插入圖片描述
看起來好像比較理想,正常樣本一般在0-30的區間,而異常樣本一般是40或更大。
如果我們希望異常更容易被發現,就設置更高的閾值,如果不希望系統誤報,則設置低閾值。下面用測試集測試準確率。

y_test = t_test_origin==0

X_test = torch.tensor(X_test).float()
err = Reconstruct_error(autoencoder, X_test)
yhat = err>35

anom_test = yhat[y_test]
anomaly_accuracy = np.sum(anom_test)/len(anom_test)
nom_test = ~yhat[~(y_test)]
normal_accuracy = np.sum(nom_test)/len(nom_test)
print("Normal accuracy:",normal_accuracy)
print("Anomaly accuracy:",anomaly_accuracy)

Normal accuracy: 0.8682926829268293
Anomaly accuracy: 0.9622448979591837

看起來取得了不錯的效果。

總結

無監督的機器學習任務自由度很高,我們可以嘗試各種各樣的方法去輔助我們開展工作。並沒有最好的算法,最貼合任務的算法纔是最好的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章