2020. 8. 23. 14:27ㆍ분석 Python/Pytorch
LSTM AutoEncoder를 사용해서 희귀케이스 잡아내기
기존에는 LSTM AutoEncoder에 대한 설명이라면, 이번에는 Pytorch로 구현을 해보고자 했다.
물론 잘못된 것이 있을 수 있으니, 피드백 주면 수정하겠다.
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)
데이터는 아래 주소를 참고하면 된다.
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 정하는 거나 다른 것은 다음에...
참고
'분석 Python > Pytorch' 카테고리의 다른 글
[Pytorch] torch에서 모델 summary 확인하는 방법 (0) | 2020.08.25 |
---|---|
[Pytorch] Pytorch를 Keras처럼 API 호출 하는 방식으로 사용하는 방법 (0) | 2020.08.25 |
Pytorch 1.6 Release Note Information (0) | 2020.08.21 |
[Pytorch] MixtureSameFamily 을 사용해서 bimodal distribution 만들기 (0) | 2020.05.05 |
[Pytorch] 1.5.0 버전 설치하기 (0) | 2020.05.05 |