[Pytorch] LSTM AutoEncoder for Anomaly Detection

2020. 8. 23. 14:27분석 Python/Pytorch

728x90

LSTM AutoEncoder를 사용해서 희귀케이스 잡아내기

 

LSTM AutoEncoder를 사용해서 희귀케이스 잡아내기

도움이 되셨다면, 광고 한번만 눌러주세요. 블로그 관리에 큰 힘이 됩니다 ^^ 우리 데이터는 많은데, 희귀 케이스는 적을 때 딥러닝 방법을 쓰고 싶을 때, AutoEncoder를 사용해서 희귀한 것에 대해��

data-newbie.tistory.com

기존에는 LSTM AutoEncoder에 대한 설명이라면, 이번에는 Pytorch로 구현을 해보고자 했다.

물론 잘못된 것이 있을 수 있으니, 피드백 주면 수정하겠다.

https://jaehyeongan.github.io/2020/02/29/LSTM-Autoencoder-for-Anomaly-Detection/

Anomaley Detection을 당일날 맞추면 의미가 없으므로 시점을 이동시키는 작업을 하고, 이동시킨 데이터를 이용해 LSTM AutoEncoder를 진행해보고자 한다.

 

여기서 Curve Shifting이라는 말이 나오는데, 사전 예측 개념을 적용하기 위해서 다음과 같은 과정을 하는 것 말한다.

01-04일 날 발생했다고 하면, 사전에 이러한 조짐이 발생했을 것이고, 여기서는 그러한 조짐을 2일 전부터 발생할 것이라고 가정하고 아래와 같이 01-02,01-03에 라벨을 붙여고 01-04는 당일 발생했으니, 제거해준다.

import pandas as pd 
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from pylab import rcParams
from collections import Counter
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn import metrics
import copy

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
torch.manual_seed(1)

데이터는 아래 주소를 참고하면 된다.

raw.githubusercontent.com/robertoamansur/rare_event_pred_maintanance/master/processminer-rare-event-mts%20-%20data.csv

import pandas as pd
# file_path = "https://raw.githubusercontent.com/robertoamansur/rare_event_pred_maintanance/master/processminer-rare-event-mts%20-%20data.csv"
# df = pd.read_csv(file_path, sep=";")
# df.to_feather("./../DATA/processminer-rare-event-mts.ftr")
df = pd.read_feather("./../DATA/processminer-rare-event-mts.ftr")

은근히 커서 읽는데, 오래 걸리기 때문에 ftr 형식으로 바꿔서 진행해줬다.

Counter(df['y'])

Curve Shifting

sign = lambda x: (1, -1)[x < 0]
def curve_shift(df, shift_by):
    vector = df['y'].copy()
    for s in range(abs(shift_by)):
        tmp = vector.shift(sign(shift_by))
        tmp = tmp.fillna(0)
        vector += tmp
    labelcol = 'y'
    df.insert(loc=0, column=labelcol+'tmp', value=vector)
    df = df.drop(df[df[labelcol] == 1].index)
    df = df.drop(labelcol, axis=1)
    df = df.rename(columns={labelcol+'tmp': labelcol})
    df.loc[df[labelcol] > 0, labelcol] = 1
    return df
from copy import deepcopy
df_ = deepcopy(df)
shifted_df = curve_shift(df_, shift_by=-2)

CurveShifting을 2번 적용해서 총 4분 전 조기 경보를 하는 식으로 구성하는 것을 하게 되면 다음과 같다.

실제 위에 예시를 보면 259 행은 사라지고, 257,258행에 y=1이 붙은 것을 알 수 있다.

이런 식으로 사전에 그러한 특징이 발생했다고 가정하고, AutoEncoder를 진행한다.

 

# drop remove columns
shifted_df = shifted_df.drop(['time','x28','x61'], axis=1)

# x, y
input_x = shifted_df.drop('y', axis=1).values
input_y = shifted_df['y'].values

n_features = input_x.shape[1]

LSTM 은 (batch_size, timesteps, feature)에 해당하는 3d 차원의 shape을 가지므로

기존에 있던 데이터에 Timestamp를 적용한다.

여기서는 timestep을 5로 잡았다(5x2) 10분

def temporalize(X, y, timesteps):
    output_X = []
    output_y = []
    for i in range(len(X) - timesteps - 1):
        t = []
        for j in range(1, timesteps + 1):
            # Gather the past records upto the lookback period
            t.append(X[[(i + j + 1)], :])
        output_X.append(t)
        output_y.append(y[i + timesteps + 1])
    return np.squeeze(np.array(output_X)), np.array(output_y)
timesteps = 5
# Temporalize
x, y = temporalize(input_x, input_y, timesteps)
print(x.shape) # (18268, 5, 59)

데이터를 쪼갠다 

# Split into train, valid, and test 
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
x_train, x_valid, y_train, y_valid = train_test_split(x_train, y_train, test_size=0.2)

print(len(x_train))  # 11691
print(len(x_valid))  # 2923
print(len(x_test))   # 3654

정상 데이터로만 학습하기 위해서 정상 데이터만 또 따로 추출한다.

# For training the autoencoder, split 0 / 1
x_train_y0 = x_train[y_train == 0]
x_train_y1 = x_train[y_train == 1]

x_valid_y0 = x_valid[y_valid == 0]
x_valid_y1 = x_valid[y_valid == 1]

그리고 표준화(Standardize)를 적용한다.

(이 부분은 기존 코드랑은 좀 다르다)

def flatten(x) :
    num_instances, num_time_steps, num_features = x.shape
    x = np.reshape(x, newshape=(-1, num_features))
    return x 

def scale(x,scaler) :
    scaled_x = scaler.transform(x)
    return scaled_x

def reshape(scaled_x , x) :
    num_instances, num_time_steps, num_features = x.shape
    reshaped_scaled_x =\
    np.reshape(scaled_x, newshape=(num_instances, num_time_steps, num_features))
    return reshaped_scaled_x

전처리 적용

scaler = StandardScaler().fit(flatten(x_train_y0))
x_train_y0_scaled = reshape(scale(flatten(x_train_y0), scaler),x_train_y0)
x_valid_scaled = reshape(scale(flatten(x_valid), scaler),x_valid)
x_valid_y0_scaled = reshape(scale(flatten(x_valid_y0), scaler),x_valid_y0)
x_test_scaled = reshape(scale(flatten(x_test), scaler),x_test)

그래서 지금은 timestep은 5이면서, input_dim은 59 변수를 사용한다.

timesteps =  x_train_y0_scaled.shape[1] # equal to the lookback
n_features =  x_train_y0_scaled.shape[2] # 59
timesteps , n_features

아래와 같은 아키텍처를 구성하기 위해 Encoder Decoder를 나누고 전체를 아우로는 모델 코드를 찾아서 변형해봤다.

(이 부분이 정확하지 않을 수 있으니 의심하면서 보시길 바람, 의심하시고 수정이 필요한 부분 꼭 말씀해주세요!)

Encoder

class Encoder(nn.Module):
    def __init__(self, seq_len, n_features, embedding_dim=64):
        super(Encoder, self).__init__()
        self.seq_len, self.n_features = seq_len, n_features
        self.embedding_dim, self.hidden_dim = (
            embedding_dim, 2 * embedding_dim
        )
        self.rnn1 = nn.LSTM(
          input_size=n_features,
          hidden_size=self.hidden_dim,
          num_layers=1,
          batch_first=True
        )
        self.rnn2 = nn.LSTM(
          input_size=self.hidden_dim,
          hidden_size=embedding_dim,
          num_layers=1,
          batch_first=True
        )
    def forward(self, x):
        x, (_, _) = self.rnn1(x)
        x, (hidden_n, _) = self.rnn2(x)
        return  x[:,-1,:]

LSTM을 사용하다 보면 TimeDistributed가 사용된다.

하지만 Pytorch 기본 모듈에서는 TimeDistributed가 제공되지 않아서, 누군가 구현한 것을 사용했다.

어떤 사람들은 CNN으로 할 수 있다고도 하는데, 일단 누가 만들어 놓은 게 편하므로 사용함.

TimeDistributed

class TimeDistributed(nn.Module):
    def __init__(self, module, batch_first=False):
        super(TimeDistributed, self).__init__()
        self.module = module
        self.batch_first = batch_first

    def forward(self, x):
        if len(x.size()) <= 2:
            return self.module(x)
        # Squash samples and timesteps into a single axis
        x_reshape = x.contiguous().view(-1, x.size(-1))  # (samples * timesteps, input_size)
        y = self.module(x_reshape)
        # We have to reshape Y
        if self.batch_first:
            y = y.contiguous().view(x.size(0), -1, y.size(-1))  # (samples, timesteps, output_size)
        else:
            y = y.view(-1, x.size(1), y.size(-1))  # (timesteps, samples, output_size)
        return y

Decoder

class Decoder(nn.Module):
    def __init__(self, seq_len, input_dim=64, n_features=1):
        super(Decoder, self).__init__()
        self.seq_len, self.input_dim = seq_len, input_dim
        self.hidden_dim, self.n_features = 2 * input_dim, n_features
        self.rnn1 = nn.LSTM(
          input_size=input_dim,
          hidden_size=input_dim,
          num_layers=1,
          batch_first=True
        )
        self.rnn2 = nn.LSTM(
          input_size=input_dim,
          hidden_size=self.hidden_dim,
          num_layers=1,
          batch_first=True
        )
        self.output_layer = torch.nn.Linear(self.hidden_dim, n_features)
        self.timedist = TimeDistributed(self.output_layer)
        
    def forward(self, x):
        x=x.reshape(-1,1,self.input_dim).repeat(1,self.seq_len,1)       
        x, (hidden_n, cell_n) = self.rnn1(x)
        x, (hidden_n, cell_n) = self.rnn2(x)
        return self.timedist(x)

Overall Architecture

class RecurrentAutoencoder(nn.Module):
    def __init__(self, seq_len, n_features, embedding_dim=64):
        super(RecurrentAutoencoder, self).__init__()
        self.encoder = Encoder(seq_len, n_features, embedding_dim)#.to(device)
        self.decoder = Decoder(seq_len, embedding_dim, n_features)#.to(device)
    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x

 

device = torch.device("cpu")
model = RecurrentAutoencoder(timesteps, n_features, 128)
model = model.to(device)

DataLoader

class AutoencoderDataset(Dataset): 
    def __init__(self,x):
        self.x = x
    def __len__(self): 
        return len(self.x)
    def __getitem__(self, idx): 
        x = torch.FloatTensor(self.x[idx,:,:])
        return x

Training

여기서 Loss Function은 L1을 사용함. 

def train_model(model, train_dataset, val_dataset, n_epochs,batch_size):
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
    criterion = nn.L1Loss(reduction='sum').to(device)
    history = dict(train=[], val=[])
    best_model_wts = copy.deepcopy(model.state_dict())
    best_loss = 10000.0
    print("start!")
    train_dataset_ae = AutoencoderDataset(train_dataset)
    tr_dataloader = DataLoader(train_dataset_ae, batch_size=batch_size, 
                               shuffle=False,num_workers=8)
    val_dataset_ae = AutoencoderDataset(val_dataset)
    va_dataloader = DataLoader(val_dataset_ae, batch_size=len(val_dataset),
                               shuffle=False,num_workers=8)
    for epoch in range(1, n_epochs + 1):
        model = model.train()
        train_losses = []
        for batch_idx, batch_x in enumerate(tr_dataloader):
            optimizer.zero_grad()
            batch_x_tensor = batch_x.to(device)
            seq_pred = model(batch_x_tensor)
            loss = criterion(seq_pred, batch_x_tensor)
            loss.backward()
            optimizer.step()
            train_losses.append(loss.item())
        val_losses = []
        model = model.eval()
        with torch.no_grad():
            va_x  =next(va_dataloader.__iter__())
            va_x_tensor = va_x.to(device)
            seq_pred = model(va_x_tensor)
            loss = criterion(seq_pred, va_x_tensor)
            val_losses.append(loss.item())
        train_loss = np.mean(train_losses)
        val_loss = np.mean(val_losses)
        history['train'].append(train_loss)
        history['val'].append(val_loss)
        if val_loss < best_loss:
            best_loss = val_loss
            best_model_wts = copy.deepcopy(model.state_dict())
        print(f'Epoch {epoch}: train loss {train_loss} val loss {val_loss}')
    model.load_state_dict(best_model_wts)
    return model.eval(), history
model, history = train_model(model, x_train_y0_scaled , x_train_y0_scaled , 
                             n_epochs = 500, batch_size=50)

plt.plot(range(len(history["val"])), history["val"] ) 
MODEL_PATH = './lstm_ae_model.pth'
torch.save(model, MODEL_PATH)
model = torch.load(MODEL_PATH)
model = model.to(device)
def predict(model, dataset,batch_size=1):
    predictions, losses = [], []
    criterion = nn.L1Loss(reduction='sum').to(device)
    dataset_ae = AutoencoderDataset(dataset)
    dataloader_ae = DataLoader(dataset_ae, 
                               batch_size=batch_size,
                               shuffle=False,num_workers=8)
    with torch.no_grad():
        model = model.eval()
        if batch_size == len(dataset) :
            seq_true  =next(dataloader_ae.__iter__())
            seq_true = seq_true.to(device)
            seq_pred = model(seq_true)
            loss = criterion(seq_pred, seq_true)
            predictions.append(seq_pred.cpu().numpy().flatten())
            losses.append(loss.item())
        else :
            for idx , seq_true in enumerate(dataloader_ae):
                seq_true = seq_true.to(device)
                seq_pred = model(seq_true)
                loss = criterion(seq_pred, seq_true)
                predictions.append(seq_pred.cpu().numpy().flatten())
                losses.append(loss.item())
    return predictions, losses
_, losses = predict(model, x_test_scaled)
sns.distplot(losses, bins=50, kde=True);

 


실제 Cut Off 정하는 거나 다른 것은 다음에...

 

 

 

 

 

 

 


참고

lstm autoencoder pytorch code

lstm pytorch code

Using Standardscaler on 3D data

annotated_encoder_decoder

return_seqences

728x90