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

作者: Reatris

日期: 2021.01

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

一、环境配置

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

  1. # 导入 paddle
  2. import paddle
  3. import paddle.nn.functional as F
  4. print(paddle.__version__)
  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")

二、数据加载

2.1 下载数据集

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

  • 该数据集已经挂载到AI Studio,相应的项目也已经挂载数据集基于AUTOENCODER实现异常时序检测

  • 我们将使用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

2.2 数据可视化

  1. #正常的时序数据可视化
  2. fig, ax = plt.subplots()
  3. df_small_noise.plot(legend=False, ax=ax)
  4. plt.show()

../../../_images/AutoEncoder_8_0.png

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

异常时序数据的作用是待训练好模型后,我们将使用以下数据进行测试,并查看数据中的突然跳升是否被检测为异常。

  1. #异常的时序数据可视化
  2. fig, ax = plt.subplots()
  3. df_daily_jumpsup.plot(legend=False, ax=ax)
  4. plt.show()

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

2.3 数据预处理

  • 我们的训练数据包含了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

2.4 创建 Dataset

从训练数据中创建组合时间步骤为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. 步骤四:实现__len__方法,返回数据集总数目
  33. """
  34. return len(self.data)
  35. # 实例化数据集
  36. 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 来计算损失值。

  • 使用 paddle.io.DataLoader 来实现数据加载。

  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. 100%|██████████| 200/200 [00:53<00:00, 3.76it/s]

../../../_images/AutoEncoder_18_2.png

五、模型预测:探测异常时序

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

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

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

  1. # 计算阀值
  2. param_dict = paddle.load('model') # 读取保存的参数
  3. model = AutoEncoder()
  4. model.load_dict(param_dict) # 加载参数
  5. model.eval() # 预测
  6. total_loss = []
  7. datas = []
  8. # 预测所有正常时序
  9. mse_loss = paddle.nn.loss.MSELoss()
  10. # 这里设置batch_size为1,单独求得每个数据的loss
  11. data_reader = paddle.io.DataLoader(train_dataset,
  12. places=[paddle.CPUPlace()],
  13. batch_size=1,
  14. shuffle=False,
  15. drop_last=False,
  16. num_workers=0)
  17. for batch_id, data in enumerate(data_reader()):
  18. x = data[0]
  19. y = data[1]
  20. out = model(x)
  21. avg_loss = mse_loss(out,(y[:,:,:-1]))
  22. total_loss.append(avg_loss.numpy()[0])
  23. datas.append(batch_id)
  24. plt.bar(datas, total_loss)
  25. plt.ylabel("reconstruction loss")
  26. plt.xlabel("data samples")
  27. plt.show()
  28. # 获取重建loss的阀值
  29. threshold = np.max(total_loss)
  30. 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_0.png ../../../_images/AutoEncoder_22_1.png ../../../_images/AutoEncoder_22_2.png ../../../_images/AutoEncoder_22_3.png ../../../_images/AutoEncoder_22_4.png ../../../_images/AutoEncoder_22_5.png ../../../_images/AutoEncoder_22_6.png ../../../_images/AutoEncoder_22_7.png ../../../_images/AutoEncoder_22_8.png ../../../_images/AutoEncoder_22_9.png ../../../_images/AutoEncoder_22_10.png ../../../_images/AutoEncoder_22_11.png ../../../_images/AutoEncoder_22_12.png ../../../_images/AutoEncoder_22_13.png ../../../_images/AutoEncoder_22_14.png ../../../_images/AutoEncoder_22_15.png ../../../_images/AutoEncoder_22_16.png ../../../_images/AutoEncoder_22_17.png ../../../_images/AutoEncoder_22_18.png ../../../_images/AutoEncoder_22_19.png ../../../_images/AutoEncoder_22_20.png ../../../_images/AutoEncoder_22_21.png ../../../_images/AutoEncoder_22_22.png ../../../_images/AutoEncoder_22_23.png ../../../_images/AutoEncoder_22_24.png ../../../_images/AutoEncoder_22_25.png ../../../_images/AutoEncoder_22_26.png ../../../_images/AutoEncoder_22_27.png ../../../_images/AutoEncoder_22_28.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天发生了异常

../../../_images/AutoEncoder_24_0.png

  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
  2. [2990, 2992, 2993, 2994, 2995, 2998, 3000, 3001, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3015, 3016, 3017, 3018, 3019, 3020, 3021, 3022, 3023, 3024, 3025, 3026, 3027, 3028, 3029, 3030, 3031, 3032, 3033, 3034, 3035, 3036, 3037, 3038, 3039, 3040, 3041, 3042, 3043, 3044, 3045, 3046, 3047, 3048, 3049, 3050, 3051, 3052, 3053, 3054, 3055, 3056, 3057, 3058, 3059, 3060, 3061, 3062, 3063, 3064, 3065, 3066, 3067, 3068, 3069, 3070, 3071, 3072, 3073, 3074, 3075, 3076, 3077, 3078, 3079, 3080, 3081, 3082, 3083, 3084, 3085, 3086, 3087, 3088, 3089, 3090, 3091, 3092, 3093, 3094, 3095, 3096, 3097, 3098, 3099, 3100, 3101, 3102, 3103, 3104, 3105, 3106, 3107, 3108, 3109, 3110, 3111, 3112, 3113, 3114, 3115, 3116, 3117, 3118, 3119, 3120, 3121, 3122, 3123, 3124, 3125, 3126, 3127, 3128, 3129, 3130, 3131, 3132, 3133, 3134, 3135]
  1. # 异常检测结果可视化
  2. df_subset = df_daily_jumpsup.iloc[abnormal_index]
  3. fig, ax = plt.subplots()
  4. df_daily_jumpsup.plot(legend=False, ax=ax)
  5. df_subset.plot(legend=False, ax=ax, color="r")
  6. plt.show()

../../../_images/AutoEncoder_26_0.png