포스트

[Data Analysis] Autoencoder 이상 탐지

Autoencoder로 이상탐지 실습

[Data Analysis] Autoencoder 이상 탐지

Autoencoder 이상 탐지

Autoencoder란 입력 데이터를 주요 특징으로 효율적으로 압축(인코딩)한 후 이 압축된 표현에서 원본 입력을 재구성(디코딩)하는 딥러닝 기술입니다.

이를 통해 비정상적인 데이터가 입력되면 정상데이터만 학습한 Autoencoder는 noise부분은 제외하고 데이터를 출력할 것입니다. 출력된 데이터가 입력된 데이터와의 차이가 커지게 될 것이고 loss(=MSE)가 커져 threshold가 넘으면 비정상으로 판단할 것입니다.

autoencoder

위 그림과 같이 정상적인 데이터만 학습한 모델에 정상적인 데이터를 입력하고 출력값을 비교하면 Error가 크지 않는 것을 볼 수 있습니다.

autoencoder-anomaly

반면, 비정상적인 데이터를 입력하고 출력값을 비교하면 Error가 크게 띄는 것을 확인할 수 있습니다.

Autoencoder 구조 요소

  • 인코더(Encoder) : 차원 감소를 통해 입력 데이터의 압축된 표현을 인코딩하는 레이어로 구성됩니다. 일반적인 오토인코더에서 신경망의 숨겨진 레이어는 입력 레이어보다 점점 더 적은 수의 노드를 포함하며 데이터가 인코더 레이어를 통과할 때 더 작은 차원으로 ‘압축’되는 과정을 통해 압축됩니다.

  • 병목 현상(Bottleneck) : 인코더 네트워크의 출력 레이어이자 디코더 네트워크의 입력 레이어로, 입력을 가장 압축적으로 표현한 것입니다. 오코인코더의 설계 및 학습의 기본 목표는 입력 데이터를 효과적으로 재구성하는 데 필요한 최소한의 중요한 특징(또는 차원)을 발견하는 것입니다. 그런 다음 이 계층에서 나타나는 잠재 공간 표현, 즉 코드가 디코더에 입력됩니다.

  • 디코더(Decoder) : 인코딩된 데이터 표현을 압축 해제(또는 디코딩)하여 궁극적으로 데이터를 인코딩 전의 원본 형태로 재구성하며, 점진적으로 더 많은 수의 노드가 있는 숨겨진 레이어로 구성됩니다. 그런 다음 이렇게 재구성된 출력을 ‘근거가 되는 진실(대부분의 경우 단순히 원본 입력)’과 비교하여 오토인코더의 효율성을 측정합니다. 출력과 근거 진실의 차이를 재구성 오류라고 합니다.

  • 장점
    • 데이터의 Lable이 존재하지 않아도 사용 가능
    • 고차원 데이터의 특징 추출 가능
    • Autoencoder기반 다양한 알고리즘 존재(ex. 희소 오토인코더, 합성곱 오토인코더, 잡음 제거 오토인코더 등)
  • 단점
    • Hyper parameter(hidden layer) 설정이 어려움
    • Loss(MSE)에 대한 threshold 설정이 어려움

Autoencoder 실습

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pandas as pd
import numpy as np

import plotly.graph_objs as go
import plotly.io as pio
pio.renderers.default = "colab"

from sklearn.metrics import accuracy_score, roc_auc_score, f1_score, precision_score, recall_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LogisticRegression
from sklearn.manifold import TSNE

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import TensorDataset, DataLoader, SubsetRandomSampler

from fastprogress import master_bar, progress_bar

from IPython.display import display
import random
1
2
3
4
5
6
7
8
SEED = 7

torch.manual_seed(SEED)
torch.cuda.manual_seed_all(SEED)
torch.backends.cudnn.deterministic = True

np.random.seed(SEED)
random.seed(SEED)
1
2
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
print(device)
1
cuda
1
2
data = pd.read_csv("creditcard.csv")
data
TimeV1V2V3V4V5V6V7V8V9V10V11V12V13V14V15V16V17V18V19V20V21V22V23V24V25V26V27V28AmountClass
00.0-1.359807-0.0727812.5363471.378155-0.3383210.4623880.2395990.0986980.3637870.090794-0.551600-0.617801-0.991390-0.3111691.468177-0.4704010.2079710.0257910.4039930.251412-0.0183070.277838-0.1104740.0669280.128539-0.1891150.133558-0.021053149.620
10.01.1918570.2661510.1664800.4481540.060018-0.082361-0.0788030.085102-0.255425-0.1669741.6127271.0652350.489095-0.1437720.6355580.463917-0.114805-0.183361-0.145783-0.069083-0.225775-0.6386720.101288-0.3398460.1671700.125895-0.0089830.0147242.690
21.0-1.358354-1.3401631.7732090.379780-0.5031981.8004990.7914610.247676-1.5146540.2076430.6245010.0660840.717293-0.1659462.345865-2.8900831.109969-0.121359-2.2618570.5249800.2479980.7716790.909412-0.689281-0.327642-0.139097-0.055353-0.059752378.660
31.0-0.966272-0.1852261.792993-0.863291-0.0103091.2472030.2376090.377436-1.387024-0.054952-0.2264870.1782280.507757-0.287924-0.631418-1.059647-0.6840931.965775-1.232622-0.208038-0.1083000.005274-0.190321-1.1755750.647376-0.2219290.0627230.061458123.500
42.0-1.1582330.8777371.5487180.403034-0.4071930.0959210.592941-0.2705330.8177390.753074-0.8228430.5381961.345852-1.1196700.175121-0.451449-0.237033-0.0381950.8034870.408542-0.0094310.798278-0.1374580.141267-0.2060100.5022920.2194220.21515369.990
................................................................................................
284802172786.0-11.88111810.071785-9.834783-2.066656-5.364473-2.606837-4.9182157.3053341.9144284.356170-1.5931052.711941-0.6892564.626942-0.9244591.1076411.9916910.510632-0.6829201.4758290.2134540.1118641.014480-0.5093481.4368070.2500340.9436510.8237310.770
284803172787.0-0.732789-0.0550802.035030-0.7385890.8682291.0584150.0243300.2948690.584800-0.975926-0.1501890.9158021.214756-0.6751431.164931-0.711757-0.025693-1.221179-1.5455560.0596160.2142050.9243840.012463-1.016226-0.606624-0.3952550.068472-0.05352724.790
284804172788.01.919565-0.301254-3.249640-0.5578282.6305153.031260-0.2968270.7084170.432454-0.4847820.4116140.063119-0.183699-0.5106021.3292840.1407160.3135020.395652-0.5772520.0013960.2320450.578229-0.0375010.6401340.265745-0.0873710.004455-0.02656167.880
284805172788.0-0.2404400.5304830.7025100.689799-0.3779610.623708-0.6861800.6791450.392087-0.399126-1.933849-0.962886-1.0420820.4496241.962563-0.6085770.5099281.1139812.8978490.1274340.2652450.800049-0.1632980.123205-0.5691590.5466680.1088210.10453310.000
284806172792.0-0.533413-0.1897330.703337-0.506271-0.012546-0.6496171.577006-0.4146500.486180-0.915427-1.040458-0.031513-0.188093-0.0843160.041333-0.302620-0.6603770.167430-0.2561170.3829480.2610570.6430780.3767770.008797-0.473649-0.818267-0.0024150.013649217.000

284807 rows × 31 columns

1
2
# 최소 전처리
data['Time'] = data['Time'] / 3600 % 24
1
data['Class'].value_counts()
count
Class
0284315
1492


t-SNE 시각화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# t-SNE을 위한 데이터 샘플링
fraud = data.loc[data['Class'] == 1]
non_fraud = data.loc[data['Class'] == 0].sample(3000, random_state=SEED)

new_data = pd.concat([fraud, non_fraud]).reset_index(drop=True)
y = new_data['Class'].values

tsne = TSNE(n_components=2, random_state=SEED)
tsne_data = tsne.fit_transform(new_data.drop(['Class'], axis=1))

traces = []
traces.append(go.Scatter(x=tsne_data[y==1, 0], y=tsne_data[y==1, 1], mode='markers', name='Fraud', marker=dict(color='red')))
traces.append(go.Scatter(x=tsne_data[y==0, 0], y=tsne_data[y==0, 1], mode='markers', name='Non-Fraud', marker=dict(color='blue')))

layout = go.Layout(title = "t-SNE Scatter Plot",
                   xaxis_title="component1",
                   yaxis_title="component2")

fig = go.Figure(data=traces, layout=layout)

fig.show()

Autoencoder 학습

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_dls(data, batch_sz, n_workers, valid_split=0.2):
    d_size = len(data)
    ixs = np.random.permutation(range(d_size))

    split = int(d_size * valid_split)
    train_ixs, valid_ixs = ixs[split:], ixs[:split]

    train_sampler = SubsetRandomSampler(train_ixs)
    valid_sampler = SubsetRandomSampler(valid_ixs)

    ds = TensorDataset(torch.from_numpy(data).float(), torch.from_numpy(data).float())

    train_dl = DataLoader(ds, batch_sz, sampler=train_sampler, num_workers=n_workers)
    valid_dl = DataLoader(ds, batch_sz, sampler=valid_sampler, num_workers=n_workers)

    return train_dl, valid_dl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# https://github.com/AnswerDotAI/fastprogress
def plot_loss_update(epoch, epochs, mb, train_loss, valid_loss):
    """ dynamically print the loss plot during the training/validation loop.
        expects epoch to start from 1.
    """
    x = range(1, epoch+2)
    y = np.concatenate((train_loss, valid_loss))
    graphs = [[x,train_loss], [x,valid_loss]]
    x_margin = 0.0001
    y_margin = 0.0005
    x_bounds = [1-x_margin, epochs+x_margin]
    y_bounds = [np.min(y)-y_margin, np.max(y)+y_margin]

    mb.update_graph(graphs, x_bounds, y_bounds)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def train(epochs, model, train_dl, valid_dl, optimizer, criterion, device):
    model = model.to(device)

    mb = master_bar(range(epochs))
    mb.write(['epoch', 'train loss', 'valid loss'], table=True)
    train_loss_plot = []
    valid_loss_plot = []

    for epoch in mb:
        model.train()
        train_loss = 0.
        for train_X, train_y in progress_bar(train_dl, parent=mb):
            train_X, train_y = train_X.to(device), train_y.to(device)
            train_out = model(train_X)
            loss = criterion(train_out, train_y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            train_loss += loss.item()
            mb.child.comment = f'{loss.item():.4f}'

        train_loss_plot.append(train_loss/len(train_dl))

        with torch.no_grad():
            model.eval()
            valid_loss = 0.
            for valid_X, valid_y in progress_bar(valid_dl, parent=mb):
                valid_X, valid_y = valid_X.to(device), valid_y.to(device)
                valid_out = model(valid_X)
                loss = criterion(valid_out, valid_y)
                valid_loss += loss.item()
                mb.child.comment = f'{loss.item():.4f}'

        valid_loss_plot.append(valid_loss/len(valid_dl))

        plot_loss_update(epoch, epochs, mb, train_loss_plot, valid_loss_plot)
        mb.write([f'{epoch+1}', f'{train_loss/len(train_dl):.6f}', f'{valid_loss/len(valid_dl):.6f}'], table=True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class AutoEncoder(nn.Module):
    def __init__(self, f_in):
        super().__init__()

        self.encoder = nn.Sequential(
            nn.Linear(f_in, 100),
            nn.Tanh(),
            nn.Dropout(0.2),
            nn.Linear(100, 70),
            nn.Tanh(),
            nn.Dropout(0.2),
            nn.Linear(70, 40)
        )
        self.decoder = nn.Sequential(
            nn.ReLU(inplace=True),
            nn.Linear(40, 40),
            nn.Tanh(),
            nn.Dropout(0.2),
            nn.Linear(40, 70),
            nn.Tanh(),
            nn.Dropout(0.2),
            nn.Linear(70, f_in)
        )

    def forward(self, x):
        return self.decoder(self.encoder(x))
1
2
3
4
5
6
7
EPOCHS = 10
BATCH_SIZE = 512
N_WORKERS = 0

model = AutoEncoder(30)
criterion = F.mse_loss
optimizer = optim.Adam(model.parameters(), lr=1e-3)
1
2
3
4
5
6
7
X = data.drop('Class', axis=1).values
y = data['Class'].values

X = MinMaxScaler().fit_transform(X)
X_nonfraud = X[y == 0]
X_fraud = X[y == 1]
train_dl, valid_dl = get_dls(X_nonfraud[:5000], BATCH_SIZE, N_WORKERS)
1
train(EPOCHS, model, train_dl, valid_dl, optimizer, criterion, device)
epochtrain lossvalid loss
10.2703800.194823
20.1292820.031349
30.0442660.011216
40.0301860.006618
50.0244960.003836
60.0197490.002219
70.0171600.002380
80.0156200.001863
90.0139450.002094
100.0132020.001787

png

평가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def print_metric(model, df, y, scaler=None):
    X_train, X_val, y_train, y_val = train_test_split(df, y, test_size=0.2, shuffle=True, random_state=SEED, stratify=y)
    mets = [accuracy_score, precision_score, recall_score, f1_score]

    if scaler is not None:
        X_train = scaler.fit_transform(X_train)
        X_val = scaler.transform(X_val)

    model.fit(X_train, y_train)
    train_preds = model.predict(X_train)
    train_probs = model.predict_proba(X_train)[:, 1]
    val_preds = model.predict(X_val)
    val_probs = model.predict_proba(X_val)[:, 1]

    train_met = pd.Series({m.__name__: m(y_train, train_preds) for m in mets})
    train_met['roc_auc'] = roc_auc_score(y_train, train_probs)
    val_met = pd.Series({m.__name__: m(y_val, val_preds) for m in mets})
    val_met['roc_auc'] = roc_auc_score(y_val, val_probs)
    met_df = pd.DataFrame()
    met_df['train'] = train_met
    met_df['valid'] = val_met

    display(met_df)
1
2
3
4
5
6
7
with torch.no_grad():
    model.eval()
    non_fraud_encoded = model.encoder(torch.from_numpy(X_nonfraud).float().to(device)).cpu().numpy()
    fraud_encoded = model.encoder(torch.from_numpy(X_fraud).float().to(device)).cpu().numpy()

encoded_X = np.append(non_fraud_encoded, fraud_encoded, axis=0)
encoded_y = np.append(np.zeros(len(non_fraud_encoded)), np.ones(len(fraud_encoded)))
1
2
3
4
5
clf = LogisticRegression(random_state=SEED)
print('Metric scores for original data:')
print_metric(clf, X, y)
print('Metric score for encoded data:')
print_metric(clf, encoded_X, encoded_y)
1
Metric scores for original data:
trainvalid
accuracy_score0.9990080.999017
precision_score0.8529410.888889
recall_score0.5152280.489796
f1_score0.6424050.631579
roc_auc0.9683920.960051
1
Metric score for encoded data:
trainvalid
accuracy_score0.9985030.998490
precision_score0.8045980.772727
recall_score0.1776650.173469
f1_score0.2910600.283333
roc_auc0.9725810.983995

 

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.