t-SNEとPCAの実行

想定として、ある時間に200~800nmまでの波長データで何かしらの値を取得できるセンサーがあり、そのログデータから異常値を予測したいものとします。

正常データとして使用するデータと異常データとして使用するデータを読み込み
t-SNEによる次元削減を行って可視化してみます。

これでデータの分布に違いがあるか判り、使用する正常データと異常データの選択の指標の一つになります。

データは訓練用データ、検証用(異常値)データ、テスト用(異常値)データ、検証用(異常値)データです。
データの内容 - ./data/train:学習に使用するノイズの少ないsin波データ - ./data/test/normal:検証とテスト兼用の正常sin波データ - ./data/test/anomaly/test:異常値のテストデータ - ./data/test/anomaly/valid:異常値の検証用データ

正常データを1、異常データを-1としてラベルを付与して可視化します。

import os
import glob
import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
plt.style.use('seaborn')

# 警告の非表示
import warnings
warnings.filterwarnings('ignore')

分析用疑似データ作成

# 保存用フォルダ作成
if not(os.path.isdir('./data')):
    os.mkdir('./data')
if not(os.path.isdir('./data/train')):
    os.mkdir('./data/train')

if not(os.path.isdir('./data/test/normal')):
    os.mkdir('./data/test/normal')
if not(os.path.isdir('./data/test')):
    os.mkdir('./data/test')

if not(os.path.isdir('data/test/anomaly')):
    os.mkdir('data/test/anomaly')
if not(os.path.isdir('data/test/anomaly/test')):
    os.mkdir('data/test/anomaly/test')
if not(os.path.isdir('data/test/anomaly/valid')):
    os.mkdir('data/test/anomaly/valid')

データ内容の確認

それぞれどのようなデータが入るかを可視化して確認してみます。

def Make_data(index, eta=0.2, outlier=3, mode='train'):
    '''
    sin波にノイズを加えたデータを作成する
    '''
    np.random.seed(42)
    data_size = 1200                                                         # データ数
    X = np.linspace(0,1, data_size)                                          # 0~1まで20個データを作成する
    noise = np.random.uniform(low= -1.0, high=1.0, size=data_size) * eta    # -1~1までの値をデータサイズ個作成し、引数倍する
    y = np.sin(2.0 * np.pi * X) + noise                                      # sin波にノイズを追加する
    # 外れ値の設定
    # 40個ごとにランダムで0~引数outlierまでの1個の値を加算
    if mode == 'test':
        for cnt in range(data_size):
            if cnt % 40 == 0:
                y[cnt] += np.random.randint(0, outlier, 1)
    plt.subplots(figsize=(16, 9))                                            # 表示サイズ指定
    plt.scatter(X, y)                                                        # 散布図
    plt.show()
    # DataFrameを引数をカラム名として作成
    df = pd.DataFrame({
        index:y
    })
    
    return df
# train
Make_data(1, 0.05).head()

f:id:sekihan_0290:20200918215431p:plain

1
0 -0.012546
1 0.050312
2 0.033680
3 0.025586
4 -0.013438
# valid_anomaly
Make_data(1, 0.15, outlier=2.5, mode='test').head()

f:id:sekihan_0290:20200918215443p:plain

1
0 -0.037638
1 0.140455
2 0.080079
3 0.045318
4 -0.082235
# test_anomaly
Make_data(1, 0.2, outlier=3, mode='test').head()

f:id:sekihan_0290:20200918215511p:plain

1
0 -0.050184
1 0.185526
2 0.103278
3 0.055184
4 -0.116633
# 可視化の内容を消去して再度宣言
def Make_data(index, eta=0.05, outlier=3, mode='train'):
    '''
    sin波にノイズを加えたデータを作成する
    '''
    np.random.seed(42)
    data_size = 1200                                                         # データ数
    X = np.linspace(0,1, data_size)                                          # 0~1まで20個データを作成する
    noise = np.random.uniform(low= -1.0, high=1.0, size=data_size) * eta    # -1~1までの値をデータサイズ個作成し、引数倍する
    y = np.sin(2.0 * np.pi * X) + noise                                      # sin波にノイズを追加する
    # 外れ値の設定
    # 100個ごとにランダムで0~引数outlierまでの1個の値を加算
    if mode == 'test':
        for cnt in range(data_size):
            if cnt % 100 == 0:
                y[cnt] += np.random.randint(0, outlier, 1)
    # DataFrameを引数をカラム名として作成
    df = pd.DataFrame({
        index:y
    })
    return df

訓練用データを作成

関数Make_data()を使用して訓練用データを作成します。
ノイズはデフォルトの0.2でデータ数160個作成します。

df_train = pd.DataFrame([])
for i in range(1, 161):
    df_base =  Make_data(i).T
    df_train = pd.concat([df_train, df_base])
# 200nm~800nmをカラムとして1桁で丸めて設定
df_train.columns =  np.round(np.linspace(200, 800, 1200), 1)
df_train.head()
200.0 200.5 201.0 201.5 202.0 202.5 203.0 203.5 204.0 204.5 ... 795.5 796.0 796.5 797.0 797.5 798.0 798.5 799.0 799.5 800.0
1 -0.012546 0.050312 0.03368 0.025586 -0.013438 -0.008202 -0.012755 0.073292 0.052022 0.067953 ... -0.090411 -0.033694 -0.052086 -0.019345 -0.071625 0.016194 0.031628 0.036407 0.019725 -0.036991
2 -0.012546 0.050312 0.03368 0.025586 -0.013438 -0.008202 -0.012755 0.073292 0.052022 0.067953 ... -0.090411 -0.033694 -0.052086 -0.019345 -0.071625 0.016194 0.031628 0.036407 0.019725 -0.036991
3 -0.012546 0.050312 0.03368 0.025586 -0.013438 -0.008202 -0.012755 0.073292 0.052022 0.067953 ... -0.090411 -0.033694 -0.052086 -0.019345 -0.071625 0.016194 0.031628 0.036407 0.019725 -0.036991
4 -0.012546 0.050312 0.03368 0.025586 -0.013438 -0.008202 -0.012755 0.073292 0.052022 0.067953 ... -0.090411 -0.033694 -0.052086 -0.019345 -0.071625 0.016194 0.031628 0.036407 0.019725 -0.036991
5 -0.012546 0.050312 0.03368 0.025586 -0.013438 -0.008202 -0.012755 0.073292 0.052022 0.067953 ... -0.090411 -0.033694 -0.052086 -0.019345 -0.071625 0.016194 0.031628 0.036407 0.019725 -0.036991

5 rows × 1200 columns

訓練データの保存

# index不要のとき
df_train.to_csv('data/train/X_train.csv', index=False)

検証とテスト兼用の正常sin波データ

ノイズはデフォルトの0.2でデータ数80個作成します。

df_normal = pd.DataFrame([])
for i in range(1, 81):
    df_base =  Make_data(i).T
    df_normal = pd.concat([df_normal, df_base])
# 200nm~800nmをカラムとして1桁で丸めて設定
df_normal.columns =  np.round(np.linspace(200, 800, 1200), 1)
df_normal.head()
200.0 200.5 201.0 201.5 202.0 202.5 203.0 203.5 204.0 204.5 ... 795.5 796.0 796.5 797.0 797.5 798.0 798.5 799.0 799.5 800.0
1 -0.012546 0.050312 0.03368 0.025586 -0.013438 -0.008202 -0.012755 0.073292 0.052022 0.067953 ... -0.090411 -0.033694 -0.052086 -0.019345 -0.071625 0.016194 0.031628 0.036407 0.019725 -0.036991
2 -0.012546 0.050312 0.03368 0.025586 -0.013438 -0.008202 -0.012755 0.073292 0.052022 0.067953 ... -0.090411 -0.033694 -0.052086 -0.019345 -0.071625 0.016194 0.031628 0.036407 0.019725 -0.036991
3 -0.012546 0.050312 0.03368 0.025586 -0.013438 -0.008202 -0.012755 0.073292 0.052022 0.067953 ... -0.090411 -0.033694 -0.052086 -0.019345 -0.071625 0.016194 0.031628 0.036407 0.019725 -0.036991
4 -0.012546 0.050312 0.03368 0.025586 -0.013438 -0.008202 -0.012755 0.073292 0.052022 0.067953 ... -0.090411 -0.033694 -0.052086 -0.019345 -0.071625 0.016194 0.031628 0.036407 0.019725 -0.036991
5 -0.012546 0.050312 0.03368 0.025586 -0.013438 -0.008202 -0.012755 0.073292 0.052022 0.067953 ... -0.090411 -0.033694 -0.052086 -0.019345 -0.071625 0.016194 0.031628 0.036407 0.019725 -0.036991

5 rows × 1200 columns

検証とテスト兼用の正常sin波データの保存

# index不要のとき
df_normal.to_csv('./data/test/normal/X_normal.csv', index=False)

検証用(異常値)データを作成

ノイズは0.15で、外れ値は2.5、データ数30個作成します。

df_valid = pd.DataFrame([])
for i in range(1, 31):
    df_base =  Make_data(i, 0.15, outlier=2.5, mode='test').T
    df_valid = pd.concat([df_valid, df_base])
# 200nm~800nmをカラムとして1桁で丸めて設定
df_valid.columns =  np.round(np.linspace(200, 800, 1200), 1)
df_valid.head()
200.0 200.5 201.0 201.5 202.0 202.5 203.0 203.5 204.0 204.5 ... 795.5 796.0 796.5 797.0 797.5 798.0 798.5 799.0 799.5 800.0
1 -0.037638 0.140455 0.080079 0.045318 -0.082235 -0.077003 -0.101138 0.146527 0.072245 0.109567 ... -0.176941 -0.017259 -0.082909 0.004838 -0.162476 0.090501 0.126326 0.130183 0.069655 -0.110974
2 -0.037638 0.140455 0.080079 0.045318 -0.082235 -0.077003 -0.101138 0.146527 0.072245 0.109567 ... -0.176941 -0.017259 -0.082909 0.004838 -0.162476 0.090501 0.126326 0.130183 0.069655 -0.110974
3 -0.037638 0.140455 0.080079 0.045318 -0.082235 -0.077003 -0.101138 0.146527 0.072245 0.109567 ... -0.176941 -0.017259 -0.082909 0.004838 -0.162476 0.090501 0.126326 0.130183 0.069655 -0.110974
4 -0.037638 0.140455 0.080079 0.045318 -0.082235 -0.077003 -0.101138 0.146527 0.072245 0.109567 ... -0.176941 -0.017259 -0.082909 0.004838 -0.162476 0.090501 0.126326 0.130183 0.069655 -0.110974
5 -0.037638 0.140455 0.080079 0.045318 -0.082235 -0.077003 -0.101138 0.146527 0.072245 0.109567 ... -0.176941 -0.017259 -0.082909 0.004838 -0.162476 0.090501 0.126326 0.130183 0.069655 -0.110974

5 rows × 1200 columns

検証(異常値)データの保存

# index不要のとき
df_valid.to_csv('./data/test/anomaly/valid/X_valid.csv', index=False)

テスト用(異常値)データを作成

ノイズはデフォルトの0.2で外れ値は3、データ数48個作成します。

df_test = pd.DataFrame([])
for i in range(1, 49):
    df_base =  Make_data(i, 0.2, outlier=3, mode='test').T
    df_test = pd.concat([df_test, df_base])
# 200nm~800nmをカラムとして1桁で丸めて設定
df_test.columns =  np.round(np.linspace(200, 800, 1200), 1)
df_test.head()
200.0 200.5 201.0 201.5 202.0 202.5 203.0 203.5 204.0 204.5 ... 795.5 796.0 796.5 797.0 797.5 798.0 798.5 799.0 799.5 800.0
1 -0.050184 0.185526 0.103278 0.055184 -0.116633 -0.111403 -0.14533 0.183145 0.082357 0.130375 ... -0.220205 -0.009042 -0.098321 0.016929 -0.207902 0.127655 0.173675 0.177071 0.09462 -0.147966
2 -0.050184 0.185526 0.103278 0.055184 -0.116633 -0.111403 -0.14533 0.183145 0.082357 0.130375 ... -0.220205 -0.009042 -0.098321 0.016929 -0.207902 0.127655 0.173675 0.177071 0.09462 -0.147966
3 -0.050184 0.185526 0.103278 0.055184 -0.116633 -0.111403 -0.14533 0.183145 0.082357 0.130375 ... -0.220205 -0.009042 -0.098321 0.016929 -0.207902 0.127655 0.173675 0.177071 0.09462 -0.147966
4 -0.050184 0.185526 0.103278 0.055184 -0.116633 -0.111403 -0.14533 0.183145 0.082357 0.130375 ... -0.220205 -0.009042 -0.098321 0.016929 -0.207902 0.127655 0.173675 0.177071 0.09462 -0.147966
5 -0.050184 0.185526 0.103278 0.055184 -0.116633 -0.111403 -0.14533 0.183145 0.082357 0.130375 ... -0.220205 -0.009042 -0.098321 0.016929 -0.207902 0.127655 0.173675 0.177071 0.09462 -0.147966

5 rows × 1200 columns

テスト用(異常値)データの保存

# index不要のとき
df_test.to_csv('./data/test/anomaly/test/X_test.csv', index=False)

データの読み込みとラベル付与

start = time.time()             # 実行開始時間の取得
"""
正常データの読み込みとラベル付与
"""
# フォルダ内のファイル一覧を取得
check_train_normal_files = glob.glob('./data/train/*')
check_test_normal_files = glob.glob('./data/test/normal/*')

X_train = pd.DataFrame([])
y_train = []
for file_name in check_train_normal_files:
    csv = pd.read_csv(file_name)
    X_train = pd.concat([X_train, csv])

    for i in range(0, len(csv)):
        y_train.append(1)

for file_name in check_test_normal_files:
    csv = pd.read_csv(file_name)
    X_train = pd.concat([X_train, csv])

    for i in range(0, len(csv)):
        y_train.append(1)
"""
異常データの読み込みとラベル付与
"""
check_anomaly1_files = glob.glob('./data/test/anomaly/test/*')
check_anomaly2_files = glob.glob('./data/test/anomaly/valid/*')

X_test = pd.DataFrame([])
y_test = []

for file_name in check_anomaly1_files:
    csv = pd.read_csv(file_name)
    X_test = pd.concat([X_test, csv])

    for i in range(0, len(csv)):
        y_test.append(-1)

for file_name in check_anomaly1_files:
    csv = pd.read_csv(file_name)
    X_test = pd.concat([X_test, csv])

    for i in range(0, len(csv)):
        y_test.append(-1)
# trainとtestのデータを一つにまとめる
df = pd.concat([X_train, X_test])
targets = y_train
targets.extend(y_test)
targets = np.array(targets)

t-SNEの実行

データ全体を使用して学習を実行してみます。

tsne = TSNE(n_components=2, random_state=42)
df_embedded = tsne.fit_transform(df)
# xとyはt-SNE分解からの2つのコンポーネントで、targetsは実際の数
tsne_df = pd.DataFrame(
    # データを縦(カラム方向)に結合、学習結果と説明変数を結合
    np.column_stack((df_embedded, targets)),
    columns=['x', 'y', 'targets']
)
# 念のため正解ラベルのint型への型変換
tsne_df.loc[:, 'targets'] = tsne_df['targets'].astype(int)
grid = sns.FacetGrid(tsne_df, hue='targets', size=8)
grid.map(plt.scatter, 'x', 'y')
# grid.set(xlim=(-20, 20), ylim=(-60, 60), xticks=[-20, -10, 0, 10, 20])
grid.add_legend()
plt.title('t-SNE scatter prot')
plt.tight_layout()
# plt.savefig('./image/t-SNE_data_check.png')
plt.show()

f:id:sekihan_0290:20200918215522p:plain

PCAの実行

データ全体を使用して学習を実行してみます。

pca = PCA(n_components=2, random_state=42)
df_embedded = pca.fit_transform(df)
# xとyはt-SNE分解からの2つのコンポーネントで、targetsは実際の数
tsne_df = pd.DataFrame(
    # データを縦(カラム方向)に結合、学習結果と説明変数を結合
    np.column_stack((df_embedded, targets)),
    columns=['x', 'y', 'targets']
)
# 念のため正解ラベルのint型への型変換
tsne_df.loc[:, 'targets'] = tsne_df['targets'].astype(int)
grid = sns.FacetGrid(tsne_df, hue='targets', size=8)
grid.map(plt.scatter, 'x', 'y')
# grid.set(xlim=(-20, 20), ylim=(-60, 60), xticks=[-20, -10, 0, 10, 20])

# seabornの凡例を使用すると結果と被るので変更
# grid.add_legend()
plt.legend(loc="best", frameon=True, edgecolor="blue")
plt.title('PCA scatter prot')
plt.tight_layout()
plt.savefig('./image/PCA_data_check.png')
plt.show()

f:id:sekihan_0290:20200918215536p:plain

普段は細かくデータの選別をするならt-SNE
よりクラスタ間の距離感をみるならPCAを重視して確認を行っています。