通过AutoEncoder实现时序数据异常检测

    日期: 2021.01

    摘要: 本示例将会演示如何使用飞桨2.0完成时序异常检测任务。这是一个较为简单的示例,将会构建一个AutoEncoder网络完成任务。

    本教程基于Paddle 2.0 编写,如果您的环境不是本版本,请先参考官网安装 Paddle 2.0 。

    1. 2.0.0
    1. # 导入其他模块
    2. import numpy as np
    3. import pandas as pd
    4. from matplotlib import pyplot as plt
    5. import warnings
    6. warnings.filterwarnings("ignore")

    二、数据加载

    • 我们将使用纽伦塔异常基准(NAB)数据集。它提供人工时间序列数据,包含标记的异常行为周期。

    • 该数据集已经挂载到AI Studio,相应的项目也已经挂载数据集

    • 我们将使用art_daily_small_noise.csv文件内数据进行训练,并使用art_day_jumpup.csv文件内数据进行测试。

    • 该数据集的简单性使我们能够有效地演示异常检测。

    1. #解压数据集
    2. # %cd ./
    3. # !unzip ./archive.zip
    1. #正常数据预览
    2. df_small_noise_path = 'artificialNoAnomaly/artificialNoAnomaly/art_daily_small_noise.csv'
    3. df_small_noise = pd.read_csv(
    4. df_small_noise_path, parse_dates=True, index_col="timestamp"
    5. )
    6. #异常数据预览
    7. df_daily_jumpsup_path = 'artificialWithAnomaly/artificialWithAnomaly/art_daily_jumpsup.csv'
    8. df_daily_jumpsup = pd.read_csv(
    9. df_daily_jumpsup_path, parse_dates=True, index_col="timestamp"
    10. )
    11. print(df_small_noise.head())
    12. print(df_daily_jumpsup.head())
    1. value
    2. timestamp
    3. 2014-04-01 00:00:00 18.324919
    4. 2014-04-01 00:05:00 21.970327
    5. 2014-04-01 00:10:00 18.624806
    6. 2014-04-01 00:15:00 21.953684
    7. 2014-04-01 00:20:00 21.909120
    8. value
    9. timestamp
    10. 2014-04-01 00:00:00 19.761252
    11. 2014-04-01 00:05:00 20.500833
    12. 2014-04-01 00:10:00 19.961641
    13. 2014-04-01 00:15:00 21.490266
    14. 2014-04-01 00:20:00 20.187739
    1. #正常的时序数据可视化
    2. fig, ax = plt.subplots()
    3. df_small_noise.plot(legend=False, ax=ax)
    4. plt.show()

    带有异常的时序数据如下:

    ../../../_images/AutoEncoder_10_0.png

    • 我们的训练数据包含了14天的采样,每天每隔5分钟采集一次数据,所以:

    • 每天包含 24 * 60 / 5 = 288 个timestep

    • 总共14天 288 * 14 = 4032 个数据

    1. #初始化并保存我们得到的均值和方差,用于初始化数据。
    2. training_mean = df_small_noise.mean()
    3. training_std = df_small_noise.std()
    4. df_training_value = (df_small_noise - training_mean) / training_std
    5. print("训练数据总量:", len(df_training_value))
    1. 训练数据总量: 4032

    从训练数据中创建组合时间步骤为288的连续数据值的序列。

    1. #时序步长
    2. TIME_STEPS = 288
    3. class MyDataset(paddle.io.Dataset):
    4. """
    5. 步骤一:继承paddle.io.Dataset类
    6. """
    7. def __init__(self,data,time_steps):
    8. """
    9. 步骤二:实现构造函数,定义数据读取方式,划分训练和测试数据集
    10. 注意:我们这个是不需要label的哦
    11. """
    12. super(MyDataset, self).__init__()
    13. self.time_steps = time_steps
    14. self.data = paddle.to_tensor(self.transform(data),dtype='float32')
    15. def transform(self,data):
    16. '''
    17. 构造时序数据
    18. '''
    19. output = []
    20. for i in range(len(data) - self.time_steps):
    21. output.append(np.reshape(data[i : (i + self.time_steps)],(1,self.time_steps)))
    22. return np.stack(output)
    23. def __getitem__(self, index):
    24. """
    25. 步骤三:实现__getitem__方法,定义指定index时如何获取数据,并返回单条数据(训练数据)
    26. """
    27. data = self.data[index]
    28. label = self.data[index]
    29. return data,label
    30. def __len__(self):
    31. """
    32. """
    33. return len(self.data)
    34. # 实例化数据集
    35. train_dataset = MyDataset(df_training_value.values,TIME_STEPS)

    接下来是构建AutoEncoder模型,本示例使用 paddle.nn 下的API,Layer、Conv1D、Conv1DTranspose、relu,采用 SubClass 的方式完成网络的搭建。

    1. class AutoEncoder(paddle.nn.Layer):
    2. def __init__(self):
    3. super(AutoEncoder, self).__init__()
    4. self.conv0 = paddle.nn.Conv1D(in_channels=1,out_channels=32,kernel_size=7,stride=2)
    5. self.conv1 = paddle.nn.Conv1D(in_channels=32,out_channels=16,kernel_size=7,stride=2)
    6. self.convT0 = paddle.nn.Conv1DTranspose(in_channels=16,out_channels=32,kernel_size=7,stride=2)
    7. self.convT1 = paddle.nn.Conv1DTranspose(in_channels=32,out_channels=1,kernel_size=7,stride=2)
    8. def forward(self, x):
    9. x = self.conv0(x)
    10. x = F.relu(x)
    11. x = F.dropout(x,0.2)
    12. x = self.conv1(x)
    13. x = F.relu(x)
    14. x = self.convT0(x)
    15. x = F.relu(x)
    16. x = F.dropout(x,0.2)
    17. x = self.convT1(x)
    18. return x

    四、模型训练

    接下来,我们用一个循环来进行模型的训练,我们将会:

    • 使用 paddle.optimizer.Adam 优化器来进行优化。

    • 使用 paddle.nn.MSELoss 来计算损失值。

    1. import tqdm
    2. #参数设置
    3. epoch_num = 200
    4. batch_size = 128
    5. learning_rate = 0.001
    6. def train():
    7. print('训练开始')
    8. #实例化模型
    9. model = AutoEncoder()
    10. #将模型转换为训练模式
    11. model.train()
    12. #设置优化器,学习率,并且把模型参数给优化器
    13. opt = paddle.optimizer.Adam(learning_rate=learning_rate,parameters=model.parameters())
    14. #设置损失函数
    15. mse_loss = paddle.nn.MSELoss()
    16. #设置数据读取器
    17. data_reader = paddle.io.DataLoader(train_dataset,
    18. batch_size=batch_size,
    19. shuffle=True,
    20. drop_last=True)
    21. history_loss = []
    22. iter_epoch = []
    23. for epoch in tqdm.tqdm(range(epoch_num)):
    24. for batch_id, data in enumerate(data_reader()):
    25. x = data[0]
    26. y = data[1]
    27. out = model(x)
    28. avg_loss = mse_loss(out,(y[:,:,:-1])) # 输入的数据经过卷积会丢掉最后一个数据
    29. avg_loss.backward()
    30. opt.step()
    31. opt.clear_grad()
    32. iter_epoch.append(epoch)
    33. history_loss.append(avg_loss.numpy()[0])
    34. #绘制loss
    35. plt.plot(iter_epoch,history_loss, label = 'loss')
    36. plt.legend()
    37. plt.xlabel('iters')
    38. plt.ylabel('Loss')
    39. plt.show()
    40. #保存模型参数
    41. paddle.save(model.state_dict(),'model')
    42. train()
    1. 训练开始

    我们将用我们训练好的模型探测异常时序:

    1. 使用自编码器计算出无异常时序数据集里的所有重建损失

    2. 找出最大重建损失并且以这个为阀值,模型重建损失超出这个值则输入的数据为异常时序

    1. # 计算阀值
    2. param_dict = paddle.load('model') # 读取保存的参数
    3. model = AutoEncoder()
    4. model.load_dict(param_dict) # 加载参数
    5. total_loss = []
    6. datas = []
    7. # 预测所有正常时序
    8. # 这里设置batch_size为1,单独求得每个数据的loss
    9. data_reader = paddle.io.DataLoader(train_dataset,
    10. places=[paddle.CPUPlace()],
    11. batch_size=1,
    12. shuffle=False,
    13. drop_last=False,
    14. num_workers=0)
    15. for batch_id, data in enumerate(data_reader()):
    16. x = data[0]
    17. y = data[1]
    18. out = model(x)
    19. avg_loss = mse_loss(out,(y[:,:,:-1]))
    20. total_loss.append(avg_loss.numpy()[0])
    21. datas.append(batch_id)
    22. plt.bar(datas, total_loss)
    23. plt.ylabel("reconstruction loss")
    24. plt.xlabel("data samples")
    25. plt.show()
    26. # 获取重建loss的阀值
    27. threshold = np.max(total_loss)
    28. print("阀值:", threshold)

    ../../../_images/AutoEncoder_20_0.png

    1. 阀值: 0.030881321

    六、AutoEncoder 对异常数据的重构

    为了好玩,让我们先看看我们的模型是如何重构第一个组数据。这是我们训练数据集第一天起的288步时间。

    1. import sys
    2. param_dict= paddle.load('model') #读取保存的参数
    3. model = AutoEncoder()
    4. model.load_dict(param_dict) #加载参数
    5. model.eval() #预测
    6. data_reader = paddle.io.DataLoader(train_dataset,
    7. places=[paddle.CPUPlace()],
    8. batch_size=128,
    9. shuffle=False,
    10. drop_last=False,
    11. num_workers=0)
    12. for batch_id, data in enumerate(data_reader()):
    13. x = data[0]
    14. out = model(x)
    15. step = np.arange(287)
    16. plt.plot(step,x[0,0,:-1].numpy())
    17. plt.plot(step,out[0,0].numpy())
    18. plt.show()
    19. sys.exit

    ../../../_images/AutoEncoder_22_1.png ../../../_images/AutoEncoder_22_3.png ../../../_images/AutoEncoder_22_5.png ../../../_images/AutoEncoder_22_7.png ../../../_images/AutoEncoder_22_9.png ../../../_images/AutoEncoder_22_11.png ../../../_images/AutoEncoder_22_13.png ../../../_images/AutoEncoder_22_15.png ../../../_images/AutoEncoder_22_17.png ../../../_images/AutoEncoder_22_19.png ../../../_images/AutoEncoder_22_21.png ../../../_images/AutoEncoder_22_23.png ../../../_images/AutoEncoder_22_25.png ../../../_images/AutoEncoder_22_27.png ../../../_images/AutoEncoder_22_29.png

    • 可以看出对正常数据的重构效果十分不错

    • 接下来我们对异常数据进行探测

    1. df_test_value = (df_daily_jumpsup - training_mean) / training_std
    2. fig, ax = plt.subplots()
    3. df_test_value.plot(legend=False, ax=ax)
    4. plt.show()
    5. #这是测试集里面的异常数据,可以看到第11~~12天发生了异常
    1. #探测异常数据
    2. threshold = 0.033 #阀值设定,即刚才求得的值
    3. param_dict = paddle.load('model') #读取保存的参数
    4. model = AutoEncoder()
    5. model.load_dict(param_dict) #加载参数
    6. model.eval() #预测
    7. mse_loss = paddle.nn.loss.MSELoss()
    8. def create_sequences(values, time_steps=288):
    9. '''
    10. 探测数据预处理
    11. '''
    12. output = []
    13. for i in range(len(values) - time_steps):
    14. output.append(values[i : (i + time_steps)])
    15. return np.stack(output)
    16. x_test = create_sequences(df_test_value.values)
    17. x = paddle.to_tensor(x_test).astype('float32')
    18. abnormal_index = [] #记录检测到异常时数据的索引
    19. for i in range(len(x_test)):
    20. input_x = paddle.reshape(x[i],(1,1,288))
    21. out = model(input_x)
    22. loss = mse_loss(input_x[:,:,:-1],out)
    23. if loss.numpy()[0]>threshold:
    24. #开始检测到异常时序列末端靠近异常点,所以我们要加上序列长度,得到真实索引位置
    25. abnormal_index.append(i+288)
    26. #不再检测异常时序列的前端靠近异常点,所以我们要减去索引长度得到异常点真实索引,为了结果明显,我们给异常位置加宽40单位
    27. abnormal_index = abnormal_index[:(-288+40)]
    28. print(len(abnormal_index))
    29. print(abnormal_index)
    1. 141