深度学习在异常检测中的应用:使用Keras和TensorFlow构建自编码器

338 阅读15分钟

在当今数据驱动的世界中,异常检测成为了各个领域中不可或缺的技术,无论是在金融、制造业还是医学领域,都需要精准地识别出那些“异常”的数据点。本教程将带您通过Keras和TensorFlow来实现深度学习中的异常检测。

在本教程的开始,讨论异常检测,包括:

  • 异常检测的挑战和重要性
  • 传统深度学习方法在异常检测中的局限性
  • 自编码器如何为异常检测提供独特的解决方案

接下来,将使用Keras和TensorFlow实现一个自编码器模型,并以无监督的方式对其进行训练。最终,将展示如何使用该模型识别出训练集、测试集及未知数据中的异常。

什么是异常检测?

异常检测指的是识别那些偏离正常模式、发生频率极低的事件。异常的具体例子包括:

  • 由于重大事件引发的股市剧烈波动
  • 生产线上检测出的缺陷产品
  • 实验室中的污染样本

通常情况下,异常事件仅占数据的0.001%到1%,极其稀少。而这也正是异常检测的挑战所在——在大量正常数据中精准找到异,类标签存在巨大的不平衡。

为了检测异常,机器学习研究人员创建了诸如隔离森林、单类SVM、椭圆包络和局部异常因子等算法来帮助检测这类事件。然而,深度学习,尤其是自编码器,正在成为处理此类问题的有力工具。自编码器是一种无监督的神经网络结构,它通过将输入数据压缩到潜在空间,并尝试从该空间中重构原始数据,从而实现对数据的表征。

深度学习和自编码器用于异常检测

自编码器是一种无监督神经网络,其核心任务是:

  • 接收输入数据
  • 将数据压缩到潜在空间表示
  • 从潜在空间中重构输入数据

这一过程依赖于两个主要组件:编码器(encoder)和解码器(decoder)。编码器将输入数据压缩至潜在空间,而解码器则尝试从潜在空间重构输入。

在训练过程中,网络的隐藏层会学习到强健的特征,即便输入数据存在噪声也能处理。然而,自编码器在异常检测中显得尤为独特的原因在于重构损失。通常,通过计算输入数据与自编码器重构数据之间的均方误差(MSE)来衡量其表现。

例如,假设在MNIST数据集上训练了一个自编码器:

图2:MNIST手写数字数据集示例

自编码器在重构这些手写数字时,表现得相当优秀。

图3:使用自编码器重构MNIST中的数字

如果计算输入数字手写体图像与重构图像之间的MSE,会发现误差相当低。

但如果让自编码器重构一张大象的照片,结果会怎样呢?

图4:自编码器对大象图像的重构

由于自编码器从未见过大象,并且未被训练来重构大象图像,因此其重构误差(MSE)会非常高。这种情况下,高MSE预示着输入数据可能是异常值

项目结构

以下是项目结构:

.
├── output
│   ├── autoencoder.model
│   └── images.pickle
├── network
│   ├── __init__.py
│   └── convautoencoder.py
├── find_anomalies.py
├── plot.png
├── recon_vis.png
└── train_unsupervised_autoencoder.py
2 directories, 8 files

convautoencoder.py文件包含ConvAutoencoder类,负责构建自编码器实现。

将在train_unsupervised_autoencoder.py中用未标记的数据训练自编码器,产生以下输出:

  • autoencoder.model:序列化的训练好的自编码器模型。
  • images.pickle:一组序列化的未标记图像,用于寻找异常。
  • plot.png:包含训练损失曲线的图表。
  • recon_vis.png:一个可视化图表,比较了真实数字图像样本与每个重构图像。

从那里开始,将在find_anomalies.py中开发一个异常检测器,并将自编码器应用于重构数据并寻找异常。

使用Keras和TensorFlow实现异常检测的自编码器

在本教程中,将基于TensorFlow 2.0实现一个用于异常检测的自编码器。

使用深度学习进行异常检测的第一步是实现自编码器脚本,卷积自编码器与之前介绍的自编码器以及去噪自编码器实现类似。

首先,打开 convautoencoder.py 文件,代码如下:

from tensorflow.keras.layers import BatchNormalization
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import Conv2DTranspose
from tensorflow.keras.layers import LeakyReLU
from tensorflow.keras.layers import Activation
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Reshape
from tensorflow.keras.layers import Input
from tensorflow.keras.models import Model
from tensorflow.keras import backend as K
import numpy as np


class ConvAutoencoder:
    @staticmethod
    def build(width, height, depth, filters=(32, 64), latentDim=16):
        # 初始化输入形状为“通道最后”
        # 通道维度本身
        inputShape = (height, width, depth)
        chanDim = -1
        # 定义编码器的输入
        inputs = Input(shape=inputShape)
        x = inputs
        # 循环遍历滤波器,构建编码器
        for f in filters:
            # 应用 CONV => RELU => BN 操作
            x = Conv2D(f, (3, 3), strides=2, padding="same")(x)
            x = LeakyReLU(alpha=0.2)(x)
            x = BatchNormalization(axis=chanDim)(x)
        # 展平网络然后构建潜在向量
        volumeSize = K.int_shape(x)
        x = Flatten()(x)
        latent = Dense(latentDim)(x)
        # 构建编码器模型
        encoder = Model(inputs, latent, name="encoder")

这里导入了TensorFlow的各个层和模型相关包,以及NumPy库。ConvAutoencoder类中的build方法接受五个参数:

  • width:输入图像的宽度。
  • height:输入图像的高度。
  • depth:图像中的通道数。
  • filters:编码器和解码器将分别学习的滤波器数量
  • latentDim:潜在空间表示的维度。

编码器的输入通过Keras函数式API定义,并通过卷积、LeakyReLU激活和批归一化层构建潜在空间表示,潜在空间表示是输入数据的压缩版本。

接下来构建解码器模型,使用相同的潜在空间表示来重构原始输入图像:

        # 构建解码器模型,接受编码器的输出作为输入
        latentInputs = Input(shape=(latentDim,))
        x = Dense(np.prod(volumeSize[1:]))(latentInputs)
        x = Reshape((volumeSize[1], volumeSize[2], volumeSize[3]))(x)
        # 反向遍历滤波器数量,构建解码器
        for f in filters[::-1]:
            # 应用 CONV_TRANSPOSE => RELU => BN 操作
            x = Conv2DTranspose(f, (3, 3), strides=2, padding="same")(x)
            x = LeakyReLU(alpha=0.2)(x)
            x = BatchNormalization(axis=chanDim)(x)
 
        # 应用单个卷积转置层恢复图像原始深度
        x = Conv2DTranspose(depth, (3, 3), padding="same")(x)
        outputs = Activation("sigmoid")(x)

        # 构建解码器模型
        decoder = Model(latentInputs, outputs, name="decoder")
        
        # 自编码器是编码器 + 解码器
        autoencoder = Model(inputs, decoder(encoder(inputs)), name="autoencoder")
        # 返回编码器、解码器和自编码器的 3 元组
        return (encoder, decoder, autoencoder)

在解码器部分,潜在输入通过全连接层重塑为3D体积(即图像数据)。滤波器数量以相反顺序遍历,通过卷积转置层将图像恢复到原始空间维度,最终构建解码器模型。最后,将编码器和解码器组合为自编码器模型,并返回这三个模型实例。

实现异常检测训练脚本

在完成自编码器实现后,现在可以继续编写训练脚本,以实现无监督的异常检测。

首先,打开项目目录中的 train_unsupervised_autoencoder.py 文件,并添加以下代码:

import matplotlib
matplotlib.use("Agg")

from network.convautoencoder import ConvAutoencoder
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.datasets import mnist
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import numpy as np
import argparse
import random
import pickle
import cv2

导入了包括 ConvAutoencoder 实现、MNIST 数据集,以及来自 TensorFlow、scikit-learn 和 OpenCV 的必要模块。

接下来,定义一个函数,用于构建无监督的数据集:

def build_unsupervised_dataset(data, labels, validLabel=1, anomalyLabel=3, contam=0.01, seed=42):
    # 获取指定类别标签和异常标签的索引
    validIdxs = np.where(labels == validLabel)[0]
    anomalyIdxs = np.where(labels == anomalyLabel)[0]

    # 随机打乱这两组索引
    random.shuffle(validIdxs)
    random.shuffle(anomalyIdxs)

    # 计算要选择的异常数据点的总数
    i = int(len(validIdxs) * contam)
    anomalyIdxs = anomalyIdxs[:i]

    # 提取有效图像和“异常”图像
    validImages = data[validIdxs]
    anomalyImages = data[anomalyIdxs]

    # 合并图像并打乱顺序
    images = np.vstack([validImages, anomalyImages])
    np.random.seed(seed)
    np.random.shuffle(images)

    # 返回无监督数据集
    return images

build_unsupervised_dataset 函数将标记数据集转换为无标签数据集,用于无监督学习。它接受数据和标签,指定有效标签和异常标签,并通过随机选择一部分异常数据来污染数据集。最后,返回混合后的无标签图像数据。

接下来,定义一个辅助函数,用于可视化自编码器的预测结果:

def visualize_predictions(decoded, gt, samples=10):
    # 初始化输出图像列表
    outputs = None

    # 循环遍历样本数量
    for i in range(samples):
        # 获取原始图像和重构图像
        original = (gt[i] * 255).astype("uint8")
        recon = (decoded[i] * 255).astype("uint8")

        # 将原始图像和重构图像并排显示
        output = np.hstack([original, recon])

        # 初始化或堆叠输出图像
        outputs = output if outputs is None else np.vstack([outputs, output])

    # 返回输出图像
    return outputs

visualize_predictions 函数将输入图像与对应的重构图像并排显示,帮助我们直观评估自编码器的性能。

接下来,解析命令行参数:

# 构建参数解析器并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", type=str, required=True, help="path to output dataset file")
ap.add_argument("-m", "--model", type=str, required=True, help="path to output trained autoencoder")
ap.add_argument("-v", "--vis", type=str, default="recon_vis.png", help="path to output reconstruction visualization file")
ap.add_argument("-p", "--plot", type=str, default="plot.png", help="path to output plot file")
args = vars(ap.parse_args())

这些命令行参数指定了输出文件路径,包括数据集、模型、可视化结果和训练曲线图。然后,准备训练数据:

# 初始化训练超参数
EPOCHS = 20
INIT_LR = 1e-3
BS = 32

# 加载 MNIST 数据集
print("[INFO] loading MNIST dataset...")
((trainX, trainY), (testX, testY)) = mnist.load_data()

# 构建无监督数据集并添加异常
print("[INFO] creating unsupervised dataset...")
images = build_unsupervised_dataset(trainX, trainY, validLabel=1, anomalyLabel=3, contam=0.01)

# 为每个图像添加通道维度,并将像素强度缩放到 [0, 1] 范围内
images = np.expand_dims(images, axis=-1)
images = images.astype("float32") / 255.0

# 构建训练和测试集
(trainX, testX) = train_test_split(images, test_size=0.2, random_state=42)

这部分代码加载 MNIST 数据集,构建无监督数据集,并进行预处理。数据集被拆分为训练和测试集。

最后,构建自编码器并进行训练:

# 构建卷积自编码器
print("[INFO] building autoencoder...")
(encoder, decoder, autoencoder) = ConvAutoencoder.build(28, 28, 1)

# 编译模型
opt = Adam(lr=INIT_LR, decay=INIT_LR / EPOCHS)
autoencoder.compile(loss="mse", optimizer=opt)

# 训练模型
H = autoencoder.fit(trainX, trainX, validation_data=(testX, testX), epochs=EPOCHS, batch_size=BS)

# 生成预测并保存可视化结果
print("[INFO] making predictions...")
decoded = autoencoder.predict(testX)
vis = visualize_predictions(decoded, testX)
cv2.imwrite(args["vis"], vis)

这段代码使用 Adam 优化器编译自编码器,并在训练集上训练模型。训练完成后,生成预测并保存可视化结果。

最后,保存训练过程和模型:

# 保存训练损失曲线
N = np.arange(0, EPOCHS)
plt.style.use("ggplot")
plt.figure()
plt.plot(N, H.history["loss"], label="train_loss")
plt.plot(N, H.history["val_loss"], label="val_loss")
plt.title("Training Loss")
plt.xlabel("Epoch #")
plt.ylabel("Loss")
plt.legend(loc="lower left")
plt.savefig(args["plot"])

# 序列化图像数据
print("[INFO] saving image data...")
with open(args["dataset"], "wb") as f:
    f.write(pickle.dumps(images))

# 保存自编码器模型
print("[INFO] saving autoencoder...")
autoencoder.save(args["model"], save_format="h5")

该部分代码绘制训练历史曲线并保存到磁盘,同时将处理后的数据和训练好的自编码器模型序列化并保存。

训练异常检测器

打开一个终端并执行以下命令启动训练::

$ python train_unsupervised_autoencoder.py \
	--dataset output/images.pickle \
	--model output/autoencoder.model

执行上述命令后,终端会显示如下输出,展示训练过程的关键阶段:

[INFO] loading MNIST dataset...
[INFO] creating unsupervised dataset...
[INFO] building autoencoder...
Train on 5447 samples, validate on 1362 samples
Epoch 1/20
5447/5447 [==============================] - 7s 1ms/sample - loss: 0.0421 - val_loss: 0.0405
Epoch 2/20
5447/5447 [==============================] - 6s 1ms/sample - loss: 0.0129 - val_loss: 0.0306
Epoch 3/20
5447/5447 [==============================] - 6s 1ms/sample - loss: 0.0045 - val_loss: 0.0088
...
Epoch 20/20
5447/5447 [==============================] - 6s 1ms/sample - loss: 0.0016 - val_loss: 0.0019
[INFO] making predictions...
[INFO] saving image data...
[INFO] saving autoencoder...

图5:训练自编码器的损失曲线

通过图5中的损失曲线,可以清楚地看到模型在整个训练过程中的表现。在一台3GHz Intel Xeon处理器上,训练20个周期大约需要2分钟,训练损失和验证损失的曲线稳定且收敛良好,这表明模型正在有效学习。

接下来,可以通过查看生成的 recon_vis.png 文件来验证自编码器是否成功学会了重构MNIST数据集中数字1的图像。图6展示了自编码器在训练后重构的手写数字1的效果。通过对比原始图像与重构图像,可以评估模型的重构质量。

图6:自编码器重构手写数字

最后,在继续下一节之前,确保 autoencoder.modelimages.pickle 文件已正确保存在输出目录中。可以通过以下命令进行验证:

$ ls output/
autoencoder.model	images.pickle

这些文件将在接下来的步骤中被使用,确保它们已经正确生成并保存。

使用自编码器实现异常/离群值检测脚本

目标是使用预训练的自编码器对数据集进行预测,计算原始输入图像与重构图像之间的均方误差(MSE),并通过MSE的分位数来识别离群值和异常。首先,打开 find_anomalies.py 文件,并添加以下代码:

from tensorflow.keras.models import load_model
import numpy as np
import argparse
import pickle
import cv2


# 构建参数解析并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", type=str, required=True,
	help="path to input image dataset file")
ap.add_argument("-m", "--model", type=str, required=True,
	help="path to trained autoencoder")
ap.add_argument("-q", "--quantile", type=float, default=0.999,
	help="q-th quantile used to identify outliers")
args = vars(ap.parse_args())

在这里导入必要的库,并设置命令行参数。load_model 来自 tf.keras,用于从磁盘加载自编码器模型。我们定义了三个参数:

  • --dataset:输入图像数据集的路径
  • --model:训练好的自编码器模型的路径
  • --quantile:用于识别离群值的分位数阈值

接下来,将加载自编码器模型和图像数据并进行预测:

# 从磁盘加载模型和图像数据
print("[INFO] loading autoencoder and image data...")
autoencoder = load_model(args["model"])
images = pickle.loads(open(args["dataset"], "rb").read())

# 对图像数据进行预测并初始化重构误差列表
decoded = autoencoder.predict(images)
errors = []

# 循环遍历所有原始图像及其相应的重构
for (image, recon) in zip(images, decoded):
	# 计算原始图像与重构图像之间的均方误差,并将其添加到误差列表中
	mse = np.mean((image - recon) ** 2)
	errors.append(mse)

在这段代码中,首先从磁盘加载自编码器模型和数据集。然后,使用模型对图像数据进行预测,并计算原始图像与重构图像之间的均方误差(MSE),最终生成一个误差列表。

通过以下代码,计算MSE的分位数阈值,并据此识别数据中的异常值:

# 计算误差的 q-th 分位数作为识别异常的阈值
thresh = np.quantile(errors, args["quantile"])
idxs = np.where(np.array(errors) >= thresh)[0]
print("[INFO] mse threshold: {}".format(thresh))
print("[INFO] {} outliers found".format(len(idxs)))

这里通过 np.quantile 计算误差的分位数,作为异常检测的阈值。任何误差大于等于该阈值的图像将被标记为离群值。最后,将异常检测结果可视化显示:

# 初始化输出数组
outputs = None

# 循环遍历具有高均方误差项的图像索引
for i in idxs:
	# 获取原始图像和重构图像
	original = (images[i] * 255).astype("uint8")
	recon = (decoded[i] * 255).astype("uint8")
	# 将原始和重构图像并排堆叠
	output = np.hstack([original, recon])

	# 如果输出数组为空,则将其初始化为当前的并排图像显示
	if outputs is None:
		outputs = output
	# 否则,垂直堆叠输出
	else:
		outputs = np.vstack([outputs, output])

# 显示输出可视化
cv2.imshow("Output", outputs)
cv2.waitKey(0)

在这部分代码中,将每个异常图像的原始图像与重构图像并排显示,并将所有结果垂直堆叠为一张输出图像。最后,使用 cv2.imshow 显示结果,可以直观地查看被标记为异常的图像。

这样,就完成了异常检测的全部流程,从加载模型和数据,到计算误差和识别异常,再到最终的可视化展示。

使用深度学习检测数据集中的异常

现在,将使用深度学习和训练好的自编码器模型来检测数据集中的异常。执行异常检测:

$ python find_anomalies.py --dataset output/images.pickle \
	--model output/autoencoder.model

执行过程中,将看到类似以下的输出:

[INFO] loading autoencoder and image data...
[INFO] mse threshold: 0.02863757349550724
[INFO] 7 outliers found

在这个例子中,使用了约0.0286的均方误差(MSE)阈值,这对应于99.9%的分位数。自编码器成功地识别了7个离群值,其中5个被正确标记为异常:

图7:展示了使用 Keras 自编码器进行数据重构时检测到的异常

尽管自编码器仅在 MNIST 数据集中对数字 1 的 1%(共 67 个样本)进行了训练,它在重构这些样本时表现出色。可以观察到,这些重构的 MSE 较高,明显高于其他样本。

此外,存在一个被错误标记为离群值的数字,这也值得进一步检查。

自编码器在发现数据集中离群值方面表现良好,即使一些图像被错误标记。对于深度学习从业者来说,正确标记但对深度神经网络架构表现不佳的图像,可能揭示了值得进一步探索的数据子集。自编码器可以帮助识别这些异常子集,从而更好地理解数据的潜在问题。

提高自编码器异常检测准确性的策略

如果自编码器在异常检测中的表现不尽如人意,可以尝试以下方法来提升性能:

  1. 数据预处理
  • 确保数据经过适当的预处理,包括标准化、归一化和去噪。干净且一致的数据有助于自编码器更好地学习特征。
  1. 模型结构调整
    • 试验不同的自编码器架构,例如增加或减少层数、更改激活函数、调整网络的深度和宽度,以寻找最佳的模型结构。
  2. 超参数优化
    • 调整学习率、批量大小和优化器等超参数。通过系统的超参数搜索(如网格搜索或随机搜索)找到最佳组合。
  3. 增加训练数据量
    • 如果条件允许,增加训练数据的数量。更多的数据可以帮助模型捕捉更丰富的特征,提高其泛化能力。
  4. 数据增强
    • 使用数据增强技术(如旋转、平移、缩放等)生成更多的训练样本。这有助于提高模型的鲁棒性和泛化能力。
  5. 特征选择
    • 优化特征选择,确保模型关注于对异常检测最有用的特征。去除冗余或无关特征可以提升模型性能。
  6. 集成学习
    • 考虑使用集成学习方法,如随机森林、梯度提升树等,这些方法可以通过结合多个模型的优势来提高检测准确性。
  7. 多模型融合
    • 尝试结合多个模型的预测结果,通过投票、堆叠等方法提升整体性能。这可以增强模型的稳定性和准确性。
  8. 深入分析
    • 对检测失败的样本进行深入分析,了解模型未能正确检测的原因。根据这些见解调整模型或数据处理流程。
  9. 关注最新研究
    • 紧跟最新的研究动态,尝试应用新兴的技术和方法。异常检测领域不断进步,新的技术可能会带来性能提升。

异常检测是一个不断发展的研究领域,目前还没有一种通用的解决方案能达到100%的准确性。因此,持续的实验和改进是必要的。保持对新技术的关注,可以在这个领域不断取得进展。

参考