[譯] 在 Keras 下使用自編碼器分類極端稀有事件

在本文中,咱們將要學習使用自編碼器搭建一個稀有事件分類器。咱們將使用來自[1]的一個現實場景稀有事件數據集。html

背景

什麼是極端稀有事件?

在稀有事件問題中,咱們面對的是一個不平衡的數據集。這表明着,相較於負樣本,咱們只有不多的正樣本標籤。典型的稀有事件問題中,正樣本在所有數據中佔比大概在 5-10% 之間。在極端稀有事件問題中,咱們只有少於 1% 的正樣本數據。好比,在咱們使用的數據集中,正樣本只有 0.6%。前端

這種極端稀有的事件在現實世界中是十分廣泛的。好比,工廠中的紙張斷裂和機器故障,在線銷售行業中的點擊或者購買。python

分類這些稀有事件是十分具備挑戰的。最近,深度學習被普遍應用於分類問題。然而,少許的正樣本限制了深度學習的應用。無論數據量有多大,正樣本數量都會限制深度學習的效果。android

爲何還要絞盡腦汁使用深度學習?

這是一個合理的問題。咱們爲何不去考慮使用其餘的機器學習方法呢?ios

答案是主觀的。咱們可使用機器學習方法。爲了使它工做,咱們能夠對負樣本數據進行負採樣,使得數據接近平衡。因爲正樣本數據只有 0.6%,降採樣後的數據集大概只有原始數據集大小的 1%。一些機器學習方法,如:SVM、隨機森林等,均可以在這個數據量上工做。然而,它的準確率會受到限制。這是由於咱們不會使用剩下的 99% 的數據。git

若是數據充足,深度學習會表現的更好。它還能夠經過使用不一樣的結構來靈活的改進模型。所以,咱們準備嘗試使用深度學習方法。github


在本推文中,咱們將要學習如何使用一個簡單的全鏈接層自編碼器來搭建一個稀有事件分類器。推文的目是爲了展現一個極端稀有事件分類器的自編碼器實現。咱們將探索不一樣自編碼器結構和配置的工做留給讀者。若是有什麼有趣的發現,請分享給咱們。後端

針對分類的自編碼器

自編碼器處理分類任務的方法相似於異常檢測。在異常檢測中,咱們學習正常過程的模式。任何與這個模式不一致的東西,咱們都認爲是異常的。對於一個稀有事件的二分類任務,咱們能夠用相似的方法使用自編碼器(延伸閱讀 [2])。框架

快速瀏覽:什麼是自編碼器?

  • 自編碼器由編碼器和解碼器組成。
  • 解碼器用來學習過程的潛在特徵。這些特徵一般是由少許的維度表出。
  • 解碼器能夠從潛在的特徵中重構出原始的數據。

Figure 1. 自編碼器的示意圖。 [[Source](http://i-systems.github.io/HSE545/machine%20learning%20all/Workshop/CAE/06_CAE_Autoencoder.html): Autoencoder by Prof. Seungchul Lee
iSystems Design Lab]

怎麼使用自編碼器來分類稀有事件?

  • 咱們首先將數據分爲兩個部分:正樣本標籤和負樣本標籤。
  • 負樣本標籤被約定爲過程的正常狀態。正常狀態是無事件的過程。
  • 咱們將忽視正樣本數據,同時在負樣本上訓練這個自編碼器。
  • 如今,這個自編碼器學習了全部正常過程的特徵。
  • 一個充分訓練的自編碼器能夠預測任何來自正常狀態的過程(由於他們有一樣的模式和分佈)。
  • 所以,重構的偏差會比較小。
  • 然而,若是咱們重構一個來自稀有事件的數據,那麼自編碼器會遇到困難。
  • 這會致使重構稀有事件時,會有一個很高的重構偏差。
  • 咱們能夠捕獲這些高重構偏差同時標記它們爲稀有事件
  • 這個過程相似於異常檢測。

實現

數據和問題

這是一個關於紙張斷裂的二分類標籤數據來自於造紙廠。在造紙廠,紙張斷裂是一個嚴重的問題。單次的紙張斷裂可能形成數千美金的損失,並且工廠天天至少會發生一次或屢次紙張斷裂。這致使每一年數百萬美圓的損失和工做風險。dom

因爲過程的性質,檢測中斷事件很是具備挑戰性。正如[1]中提到的,即便減小 5% 的斷裂也會給鋼廠帶來顯著的好處。

經過 15 天的收集,咱們獲得了包含 18K 行的數據。列 ‘y’ 包含了二分類標籤,1 表明斷裂。 其餘列是預測器。這裏有 124 個正樣本(~0.6%)。

從[這裏]下載數據(docs.google.com/forms/d/e/1…)下載數據。

代碼

Import the desired libraries.

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns

import pandas as pd
import numpy as np
from pylab import rcParams

import tensorflow as tf
from keras.models import Model, load_model
from keras.layers import Input, Dense
from keras.callbacks import ModelCheckpoint, TensorBoard
from keras import regularizers

from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, precision_recall_curve
from sklearn.metrics import recall_score, classification_report, auc, roc_curve
from sklearn.metrics import precision_recall_fscore_support, f1_score

from numpy.random import seed
seed(1)
from tensorflow import set_random_seed
set_random_seed(2)

SEED = 123 #used to help randomly select the data points
DATA_SPLIT_PCT = 0.2

rcParams['figure.figsize'] = 8, 6
LABELS = ["Normal","Break"]
複製代碼

注意,爲了可復現結果,咱們設置了隨機數種子。

數據處理

如今,咱們來讀取和準備數據。

df = pd.read_csv("data/processminer-rare-event-mts - data.csv")
複製代碼

這個稀有事件問題的目的是在發生斷裂前預測它。咱們嘗試提早 4 分鐘預測出斷裂。爲了創建這個模型,咱們把數據標籤提早 2 行(對應於 4 分鐘)。經過這行代碼實現 df.y=df.y.shift(-2)。然而,在這個問題中,咱們想作的是:判斷行 n 是否會被標記爲正樣本,

  • 讓 (n-2) 和 (n-1) 標記爲 1。這樣能夠幫助分類器學習到提早 4 分鐘預測。

  • 刪除 n 行。由於咱們不想讓分類器學習預測正在發生的斷裂。

咱們將爲這個曲線移動開發如下 UDF。

sign = lambda x: (1, -1)[x < 0]

def curve_shift(df, shift_by):
    ''' 這個函數是用來偏移數據中的二分類標籤。 平移只針對標籤爲 1 的數據 舉個例子,若是偏移量爲 -2,下面的處理將會發生: 若是是 n 行的標籤爲 1,那麼 - 使 (n+shift_by):(n+shift_by-1) = 1 - 刪除第 n 行。 也就是說標籤會上移 2 行。 輸入: df 一個分類標籤列的 pandas 數據。 這個標籤列的名字是 ‘y’。 shift_by 一個整數,表示要移動的行數。 輸出: df 按照偏移量平移事後的數據。 '''

    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
    df.insert(loc=0, column=labelcol+'tmp', value=vector)
    # 刪除 labelcol == 1 的行.
    df = df.drop(df[df[labelcol] == 1].index)
    # 丟棄 labelcol 同時將 tmp 做爲 labelcol。
    df = df.drop(labelcol, axis=1)
    df = df.rename(columns={labelcol+'tmp': labelcol})
    # 製做二分類標籤
    df.loc[df[labelcol] > 0, labelcol] = 1

    return df
複製代碼

如今,咱們將數據分爲訓練集、驗證集和測試集。而後咱們將只使用標籤爲 0 的子集來訓練自編碼器。

df_train, df_test = train_test_split(df, test_size=DATA_SPLIT_PCT, random_state=SEED)
df_train, df_valid = train_test_split(df_train, test_size=DATA_SPLIT_PCT, random_state=SEED)

df_train_0 = df_train.loc[df['y'] == 0]
df_train_1 = df_train.loc[df['y'] == 1]
df_train_0_x = df_train_0.drop(['y'], axis=1)
df_train_1_x = df_train_1.drop(['y'], axis=1)

df_valid_0 = df_valid.loc[df['y'] == 0]
df_valid_1 = df_valid.loc[df['y'] == 1]
df_valid_0_x = df_valid_0.drop(['y'], axis=1)
df_valid_1_x = df_valid_1.drop(['y'], axis=1)

df_test_0 = df_test.loc[df['y'] == 0]
df_test_1 = df_test.loc[df['y'] == 1]
df_test_0_x = df_test_0.drop(['y'], axis=1)
df_test_1_x = df_test_1.drop(['y'], axis=1)
複製代碼

標準化

對於自編碼器,一般最好使用標準化數據(轉換爲高斯、均值 0 和方差 1)。

scaler = StandardScaler().fit(df_train_0_x)
df_train_0_x_rescaled = scaler.transform(df_train_0_x)
df_valid_0_x_rescaled = scaler.transform(df_valid_0_x)
df_valid_x_rescaled = scaler.transform(df_valid.drop(['y'], axis = 1))

df_test_0_x_rescaled = scaler.transform(df_test_0_x)
df_test_x_rescaled = scaler.transform(df_test.drop(['y'], axis = 1))
複製代碼

自編碼分類器

初始化

首先,咱們將初始化自編碼器框架。咱們只構建一個簡單的自編碼器。更多複雜的結構和配置留給讀者去探索。

nb_epoch = 100
batch_size = 128
input_dim = df_train_0_x_rescaled.shape[1] #num of predictor variables, 
encoding_dim = 32
hidden_dim = int(encoding_dim / 2)
learning_rate = 1e-3

input_layer = Input(shape=(input_dim, ))
encoder = Dense(encoding_dim, activation="tanh", activity_regularizer=regularizers.l1(learning_rate))(input_layer)
encoder = Dense(hidden_dim, activation="relu")(encoder)
decoder = Dense(hidden_dim, activation='tanh')(encoder)
decoder = Dense(input_dim, activation='relu')(decoder)
autoencoder = Model(inputs=input_layer, outputs=decoder)
複製代碼

訓練

咱們將訓練模型,並保存它到指定文件。存儲訓練模型是節省將來分析時間的好方法。

autoencoder.compile(metrics=['accuracy'],
                    loss='mean_squared_error',
                    optimizer='adam')

cp = ModelCheckpoint(filepath="autoencoder_classifier.h5",
                               save_best_only=True,
                               verbose=0)

tb = TensorBoard(log_dir='./logs',
                histogram_freq=0,
                write_graph=True,
                write_images=True)

history = autoencoder.fit(df_train_0_x_rescaled, df_train_0_x_rescaled,
                    epochs=nb_epoch,
                    batch_size=batch_size,
                    shuffle=True,
                    validation_data=(df_valid_0_x_rescaled, df_valid_0_x_rescaled),
                    verbose=1,
                    callbacks=[cp, tb]).history
複製代碼

Figure 2. 自編碼器訓練過程的損失值。

分類器

接下來,咱們將展現咱們如何使用自編碼器對於稀有事件的重構偏差來作分類。

以前已經提到,若是重構偏差比較高,咱們將認定它是一次斷裂。咱們須要定一個閾值。

咱們使用驗證集來設置閾值。

valid_x_predictions = autoencoder.predict(df_valid_x_rescaled)
mse = np.mean(np.power(df_valid_x_rescaled - valid_x_predictions, 2), axis=1)
error_df = pd.DataFrame({'Reconstruction_error': mse,
                        'True_class': df_valid['y']})

precision_rt, recall_rt, threshold_rt = precision_recall_curve(error_df.True_class, error_df.Reconstruction_error)
plt.plot(threshold_rt, precision_rt[1:], label="Precision",linewidth=5)
plt.plot(threshold_rt, recall_rt[1:], label="Recall",linewidth=5)
plt.title('Precision and recall for different threshold values')
plt.xlabel('Threshold')
plt.ylabel('Precision/Recall')
plt.legend()
plt.show()
複製代碼

Figure 3. 閾值爲0.85應該在精確度和召回率之間提供一個合理的平衡。

如今,咱們將對測試數據進行分類。

咱們不該該根據測試數據來估計分類閾值。這會致使過擬合。

test_x_predictions = autoencoder.predict(df_test_x_rescaled)
mse = np.mean(np.power(df_test_x_rescaled - test_x_predictions, 2), axis=1)
error_df_test = pd.DataFrame({'Reconstruction_error': mse,
                        'True_class': df_test['y']})
error_df_test = error_df_test.reset_index()

threshold_fixed = 0.85
groups = error_df_test.groupby('True_class')

fig, ax = plt.subplots()

for name, group in groups:
    ax.plot(group.index, group.Reconstruction_error, marker='o', ms=3.5, linestyle='',
            label= "Break" if name == 1 else "Normal")
ax.hlines(threshold_fixed, ax.get_xlim()[0], ax.get_xlim()[1], colors="r", zorder=100, label='Threshold')
ax.legend()
plt.title("Reconstruction error for different classes")
plt.ylabel("Reconstruction error")
plt.xlabel("Data point index")
plt.show();
複製代碼

Figure 4. 使用閾值 = 0.85 進行分類。閾值線上方的橙色和藍色圓點分別表示真陽性和假陽性。

在圖 4 中,閾值線上方的橙色和藍色圓點分別表示真陽性和假陽性。正如咱們所看到的,咱們有不少假陽性。爲了更好的理解,咱們使用混淆矩陣來表示。

pred_y = [1 if e > threshold_fixed else 0 for e in error_df.Reconstruction_error.values]

conf_matrix = confusion_matrix(error_df.True_class, pred_y)

plt.figure(figsize=(12, 12))
sns.heatmap(conf_matrix, xticklabels=LABELS, yticklabels=LABELS, annot=True, fmt="d");
plt.title("Confusion matrix")
plt.ylabel('True class')
plt.xlabel('Predicted class')
plt.show()
複製代碼

Figure 5. 測試集預測結果的混淆矩陣。

咱們能夠預測 32 次斷裂中的 9 次。值得注意的是,這些結果是提早 2 到 4 分鐘預測的。這一比率大概是 28%,這對於造紙業來講已是一個很好的召回率了。假陽性大體是 6.3%。這並不完美,可是對於工廠而言也不壞。

該模型還能夠進一步改進,在假陽性率較小的狀況下提升召回率。咱們將在下面討論 AUC,而後討論下一個改進方法。

ROC 曲線和 AUC

false_pos_rate, true_pos_rate, thresholds = roc_curve(error_df.True_class, error_df.Reconstruction_error)
roc_auc = auc(false_pos_rate, true_pos_rate,)

plt.plot(false_pos_rate, true_pos_rate, linewidth=5, label='AUC = %0.3f'% roc_auc)
plt.plot([0,1],[0,1], linewidth=5)

plt.xlim([-0.01, 1])
plt.ylim([0, 1.01])
plt.legend(loc='lower right')
plt.title('Receiver operating characteristic curve (ROC)')
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
plt.show()
複製代碼

AUC 的結構是 0.624。

Github 倉庫

帶有註釋的代碼在這裏cran2367/autoencoder_classifier **Autoencoder model for rare event classification. Contribute to cran2367/autoencoder_classifier development by creating…**github.com

還有什麼能夠作得更好呢?

這是一個(多元)時間序列數據。咱們沒有考慮數據中的時間信息/模式。咱們將在下一篇推文探索是否能夠結合 RNN 進行分類。咱們將嘗試 LSTM autoencoder

結論

咱們研究了一個工做於造紙廠的極端稀有事件的二值數據的自編碼分類器。咱們達到了不錯的準確度。咱們的目的是展現自編碼器對於稀有事件分類問題的基礎應用。咱們以後會嘗試開發其它的方法,包括能夠結合時空特徵的 LSTM Autoencoder 來達到一個更好的效果。

下一篇關於 LSTM 自編碼的推文在這裏 LSTM Autoencoder for rare event classification.

引用

  1. Ranjan, C., Mustonen, M., Paynabar, K., & Pourak, K. (2018). Dataset: Rare Event Classification in Multivariate Time Series. arXiv preprint arXiv:1809.10717.
  2. www.datascience.com/blog/fraud-…
  3. Github repo: github.com/cran2367/au…

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索