使用 R、H20 和 MinIO 进行异常检测

使用 R、H20 和 MinIO 进行异常检测

异常检测是机器学习的一个领域,功能强大且适用于许多领域。它也可能有点像一个黑盒子,并且经常与分类混淆——我听说它被描述为将实例“分类”为“好”或“坏”。我认为值得打开黑匣子并查看内部以获得一些说明。

对于本文,我们将使用 MNIST 数据集。如果您不熟悉 MNIST,可以将其视为机器学习中用于分类问题和异常检测问题的“Hello World”。该数据集由人口普查局工作人员(通常可读的数字)和高中生(不易识别的数字)贡献的数字化手写数字组成。鉴于这种组合,它适合作为标准数据集,用于测试各种机器学习模型以及数据的预处理步骤。如果您想了解更多信息,请查看原始数据集以及围绕 MNIST 数据集的一些研究和出版物的摘要。

对于本系列的第一部分,我们将使用 H2O 异常检测和 MinIO 来存储、处理和识别数据集中的异常。本系列是我之前的帖子《使用 H20、R 和 MinIO 进行机器学习》的后续

MNIST 数据集由经过一些预处理的数字化手写数字组成。预处理描述如下:“来自 NIST 的原始黑白(二值)图像经过尺寸标准化以适合 20x20 像素框,同时保持其纵横比。由于归一化算法使用的抗锯齿技术,生成的图像包含灰度级。通过计算像素的质心并平移图像以便将此点定位在 28x28 区域的中心,图像在 28x28 图像中居中。”

这些数字通常是这样的:


pasted image 0 (76).png


并转化为0~255范围内灰度像素值的向量。

重要的是要认识到,可以通过多种方式表示此域数据(手写数字)以进行建模。灰度值网格看起来很直观(位图),但对将异常检测或任何其他机器学习方法应用于现实世界数据具有影响。有多种方法可以将该域数据转换为机器学习方法所需的数值向量,对于给定的建模方法,有些方法效果更好,有些方法效果更差。对于如何最好地表示域数据以进行建模,没有唯一的正确答案,这一点很重要。对于如何最好地预处理表示也没有正确答案。  

LeCun、Cortez 和 Burges 指出“使用某些分类方法(特别是基于模板的方法,例如 SVM 和 K 最近邻),当数字以边界框而不是质心为中心时,错误率会提高。”

纵观上述研究的总结,许多研究都使用了许多额外的预处理步骤来提高性能。

设计、执行和分析数据科学实验的意义是巨大的。任何给定领域中任何给定问题的领域数据的统计和数值建模并不是立即显而易见的。甚至专家也认为这是棘手的事情。想一想——如果我们作为人类足够了解数据中的关系,能够先验地确定如何最好地表示它,那么我们很可能已经建立了系统来准确地模拟这种关系。真正需要 ML 的问题和建模通常非常混乱和错综复杂。

足够的理论和谈话;编码时间。

用于异常检测的 H20、R 和 MinIO

有大量教程涵盖了几乎所有语言的 MNIST,以及每个机器学习平台。就个人而言,我喜欢H2O我发现它速度快、易于管理和使用,并且提供的功能非常齐全。我也喜欢在 R 中工作。当我处理数据和机器学习时,我发现 R 对我来说是最直观的。将这两者与 MinIO 对象存储相结合,为数据科学家创建了一个非常强大的平台。

如果您不熟悉 H2O,它是一个开源分布式内存机器学习环境。我在之前的一篇文章“使用 H20、R 和 MinIO 进行机器学习”中更全面地描述了 H20 。

如果您不熟悉用于统计计算的 R 编程语言,统计学家和数据挖掘人员会使用它进行数据分析。我在较早的帖子MinIO 和 Apache Arrow Using R中更全面地描述了 R。当我处理数据、数据分析和机器学习时,我发现 R 相当直观。

R 语言有一个名为 RStudio 的配套 IDE,我将使用它进行此开发:https://www.rstudio.com/IDE一般是这样的:


Untitled (56).png


MinIO是高性能软件定义的 S3 兼容对象存储,使其成为 Amazon S3 的强大而灵活的替代品。

如果你想跟随,请安装 R 和 RStudio,访问 H2O 集群,如果你还没有运行 MinIO,请下载并安装它此外,请下载并安装aws.s3 R 库

让我们开始编码吧!

在这篇博文中,我将设置 H2O、R 和 MinIO,以使用 H2O 的内置功能执行异常检测。在我之前的博文“使用 H20、R 和 MinIO 进行机器学习”中,我们使用外部 H2O 集群进行了此操作,在本文中,我们将从 R 中启动 H2O。  

我首先将原始文件下载到我的本地驱动器。这些原始文件是特定的二进制格式,需要以表格格式读入、转换和写出才能使用。我预计会针对这些文件运行许多模型和预处理方法,因此我会将它们转换并存储在一个通用 API 可访问的 MinIO 对象存储中。

首先加载所需的库和凭据,然后初始化 H20 并加载 MNIST 数据。请注意,下面一些用于显示数字以及加载和处理二进制 MNIST 文件的代码直接取自David Dalpiaz 在 github 上的代码David 是伊利诺伊大学香槟分校统计系的助教。

# Using H2O for anomaly detection

#https://docs.h2o.ai/h2o/latest-stable/h2o-r/docs/reference/h2o.anomaly.html


#h2o MNIST anomaly detection


# Starting with the raw files from http://yann.lecun.com/exdb/mnist/

#

# Four files are available on this site:

#   

# train-images-idx3-ubyte.gz:  training set images (9912422 bytes)

# train-labels-idx1-ubyte.gz:  training set labels (28881 bytes)

# t10k-images-idx3-ubyte.gz:   test set images (1648877 bytes)

# t10k-labels-idx1-ubyte.gz:   test set labels (4542 bytes)



#load necessary libraries and credentials


library(h2o)

library(aws.s3)  


# set the credentials this r instances uses to access minio

Sys.setenv("AWS_ACCESS_KEY_ID" = "minioadmin", # enter your credentials, we’re using the default

           "AWS_SECRET_ACCESS_KEY" = "minioadmin", # enter your credentials

           "AWS_DEFAULT_REGION" = "",

           "AWS_S3_ENDPOINT" = "<Your-MinIO-IP-Address>:9000")    # change it to your specific minio IP and port to override default aws s3



# initialize the h2o server

h2o.init(jvm_custom_args = "-Dsys.ai.h2o.persist.s3.endPoint=http://<Your-MinIO-IP-Address>:9000 -Dsys.ai.h2o.persist.s3.enable.path.style=true")

h2o.set_s3_credentials("minioadmin", "minioadmin")



# Some functions taken from David Dalpiaz' code on github.  David is 

# Teaching Assistant Professor for the Department of Statistics at 

# The University of Illinois at Urbana-Champaign:

# https://gist.github.com/daviddalpiaz/ae62ae5ccd0bada4b9acd6dbc9008706


# helper function for visualization

show_digit = function(arr784, col = gray(12:1 / 12), ...) {

  image(matrix(as.matrix(arr784[-785]), nrow = 28)[, 28:1], col = col, ...)

}


# load image files

load_image_file = function(filename) {

  ret = list()

  f = file(filename, 'rb')

  readBin(f, 'integer', n = 1, size = 4, endian = 'big')

  n    = readBin(f, 'integer', n = 1, size = 4, endian = 'big')

  nrow = readBin(f, 'integer', n = 1, size = 4, endian = 'big')

  ncol = readBin(f, 'integer', n = 1, size = 4, endian = 'big')

  x = readBin(f, 'integer', n = n * nrow * ncol, size = 1, signed = FALSE)

  close(f)

  data.frame(matrix(x, ncol = nrow * ncol, byrow = TRUE))

}


# load label files

load_label_file = function(filename) {

  f = file(filename, 'rb')

  readBin(f, 'integer', n = 1, size = 4, endian = 'big')

  n = readBin(f, 'integer', n = 1, size = 4, endian = 'big')

  y = readBin(f, 'integer', n = n, size = 1, signed = FALSE)

  close(f)

  y

}


# load images

train = load_image_file("data/train-images.idx3-ubyte")

test  = load_image_file("data/t10k-images.idx3-ubyte")


# load labels

train$y = as.factor(load_label_file("data/train-labels.idx1-ubyte"))

test$y  = as.factor(load_label_file("data/t10k-labels.idx1-ubyte"))


# view test image

show_digit(train[500, ])


现在数据已加载和转换,我们可以将其保存到 MinIO。使用MinIO 控制台,我创建了一个名为 的新存储桶mnist-files,并将文件写入该存储桶。这是为建模准备数据的结束:

# get the bucket

b <- get_bucket(bucket = 'mnist-files', use_https = F, region = "")


#write out the training data to MinIO S3 storage to make it centrally available.

s3write_using(train, FUN = write.csv, object = "train-images.csv", bucket = b, 

                      opts = list(use_https = FALSE, multipart = TRUE, region = ""))

s3write_using(test, FUN = write.csv, object = "test-images.csv", bucket = b, 

                      opts = list(use_https = FALSE, multipart = TRUE, region = ""))


# This is the end of the preprocessing to get the data in a reasonable format and store it so it is 

# accessible by multiple machines, all of this preprocessing would only happen once.

返回 MinIO 控制台,我们看到文件已写入我们的存储桶。


pasted image 0 (77).png


异常到底是什么?

一旦数据以 H2O 可以读取的格式集中存储,我们就可以开始使用 H2O 的异常检测功能来分析它。但首先,异常到底是什么以及自动编码器如何“检测”它们?

如果我们采用具有相同长度的输入和输出向量的 3 层神经网络,并将代表“信息”的东西(通常是图像的像素,但它可以是任何类型的信息)输入输入向量并训练网络产生如果输出向量上的结果相同,我们将教会网络逼近恒等函数(f-hat() 逼近 f(),这是恒等函数,因为我们使用输入向量作为监督训练的基本事实)

如果中间层的神经元少于输入向量,那么我们可以将输入层和中间层之间的权重矩阵视为“压缩”信息以将其存储在“更窄”的中间层中。我们还可以将中间层和输出层之间的权重矩阵视为将信息解压缩或重构回其原始值。如果我们成功地训练网络以足够低的错误率收敛,我们就可以将网络分成两部分——压缩和重建——并分别使用它们。理论上,我们可以使用压缩权重矩阵压缩大量数据,并将来自中间层神经元的值存储为信息的“压缩”版本。然后我们可以使用从中间层到输出层的权重矩阵重建原始信息。这种方法也适用于深度学习网络——具有多个隐藏层的网络。

杰出的!虽然有一个问题 - 好吧,也许有两个......

  1. 我们需要能够将网络训练到非常小的错误率(通常表示为均方误差 - MSE - 在地面真值向量(在这种情况下与输入向量相同)和预测向量之间模型在训练过程中产生以接近身份函数。MSE 需要足够低,以便当我们重建给定的信息时它不会准确,但它足够接近以至于不会混淆这是什么。

  2. 我们还必须根据系统将来会看到的大部分(如果不是全部)信息来训练模型。该模型必须能够压缩和重建我们希望使用它的所有东西。

在 Robert Hecht-Nielsen 于 1990 年出版的“神经计算”一书中,我第一次遇到了使用具有相同输入和输出向量长度的神经网络并在相同的输入和基本事实向量上对其进行训练的方法。在这本书中,作者引用了 Cottrel、Munro 和 Zipser 在 1986 年描述这种方法的论文。据我所知,这种方法在可能的医学图像之外的压缩应用不多,因为成本较低的过程可以在更广泛的数据范围内实现类似的结果。当时(1990 年),术语“自动编码器”不存在,据我所知也没有使用这种架构进行异常检测。

在某个时候,有人意识到(不确定这方面的历史)这种架构还有另一种用途。上面的第二个警告——关于必须根据模型在运行时会看到的所有信息来训练网络以实现低 MSE——可以有不同的看法。它可以被视为模型先前训练过的数据在执行时在输入向量(输入的数据)和输出向量(模型的预测)之间具有较低的 MSE。模型未经训练的数据可能具有更大的 MSE。因此,MSE 可用于识别数据是否与训练数据“相似”——用于异常检测的自动编码器诞生了!

由于自动编码器是在一组数据上训练到低 MSE,如果我们在新数据上运行自动编码器并且它报告低 MSE,那么我们知道新数据应该与训练中使用的数据相似——它是“正常的” . 如果我们在新数据上运行自动编码器并产生高 MSE,那么它与训练模型的数据不同,可以被视为“异常”。我们通常假设“异常”等同于“坏”,但事实并非如此——“异常”仅意味着我们通过自动编码器运行的数据存在于输入向量的多维空间中,与存在训练示例。这不是“坏”,只是“不同”。例如,一家公司有一个非常非常有利可图的季度可能是“异常的”,但它在主观上并不是“坏的”。

重要的是要记住异常检测不提供“好”或“坏”的指示,因此通常随后使用分类模型将“好”或“坏”分配给异常(或一些更大的分类器通过监督学习学习的一组因子值)。

综上所述,异常是输入的发生,当通过经过适当训练的自动编码器时,会导致自动编码器预测的内容与基本事实(在本例中为输入向量)之间存在高 MSE。这个高 MSE 表明输入向量与训练自动编码器的示例不相似的可能性。它在某些有意义的方面有所不同——这是一种异常。不好也不坏,只是与自动编码器训练的数据有很大不同。

已经说得够多了,回到代码。

处理数据

我们将数据写入一个通用的 MinIO 存储库,这样我们就可以让 H2O 直接从那里读取数据,而不需要让数据通过本地运行的 R 进程。下面设置了路径,数据直接从 MinIO 存储加载到服务器上运行的 H2O,并转换为 H2O 首选二进制表示。

请注意,下面的大部分代码改编自h2o-r 深度学习存储库中有关异常检测测试的部分:

# make some URLs for H2O to load the files

train_data <- "s3://mnist-files/train-images.csv"

test_data <- "s3://mnist-files/test-images.csv"


#import the file from minio directly into h2o and return a pointer to it

train_df <- h2o.importFile(path = train_data)

test_df <- h2o.importFile(path = test_data)


#convert the raw dataframe on the server to a hex file and return a pointer to it

train_hex <- as.h2o(train_df)

test_hex <- as.h2o(test_df)



一旦数据加载到 H2O 中,我们就可以开始操作它了。请记住,数据的形式是每个书写数字一行,使用 28 x 28 灰度像素值作为预测变量(前 784 列中的 784 个值)和一个响应 - 一个 10 值因子,表示数字值手写图像表示 - 0 到 9 - 在第 785 列中。我们分配这些:

predictors = c(1:784)
resp = 785

由于自动编码器使用输入向量(预测变量)作为响应,因此我们无需使用第 785 列中的值。如果我们将此数据用于预测模型,则该列将是训练和测试的基本事实放。在我们的例子中,我们可以删除它:

# If we were going to use a classifier then the dataframe is setup nicely - the predictors are all normalized
# and stored in the first columns and the response is the last column
# but this is unsupervised anomaly detection -> drop the response column (digit: 0-9)
train_hex <- h2o.assign(train_hex[,-resp], 'train_hex')
test_hex <- h2o.assign(test_hex[,-resp], 'test_hex')

此外,让我们利用一些辅助函数来可视化数字:

# helper functions for display of handwritten digits

plotDigit <- function(mydata, rec_error) {

  len <- nrow(mydata)

  N <- ceiling(sqrt(len))

  par(mfrow=c(N,N),pty='s',mar=c(1,1,1,1),xaxt='n',yaxt='n')

  for (i in 1:nrow(mydata)) {

    colors<-c('white','black')

    cus_col<-colorRampPalette(colors=colors)

    z<-array(mydata[i,],dim=c(28,28))

    z<-z[,28:1]

    image(1:28,1:28,z,main=paste0("rec_error: ", round(rec_error[i],4)),col=cus_col(256))

  }

}

plotDigits <- function(data, rec_error, row_idx) {

  my_rec_error <- rec_error[row_idx,]

  my_data <- as.matrix(as.data.frame(data[row_idx,]))

  plotDigit(my_data, my_rec_error)

}


由于数据刚刚加载到 H2O 中,因此只需一次调用即可使用 H2O 平台的功能轻松训练基本自动编码器。此调用的参数的更深入解释将在另一天进行。

# use the training data to create a deeplearning based autoencoder model
ae_model <- h2o.deeplearning(x=predictors,
                            training_frame=train_hex,
                            activation="Tanh",
                            autoencoder=TRUE,
                            hidden=c(50),
                            l1=1e-5,
                            ignore_const_cols=FALSE,
                            epochs=1)              

请记住,输入向量也是自动编码器过程的基本事实,因此 H2O 中有一个调用通过指定模型运行测试数据并计算每行的 MSE。这通常就是您想要知道的全部 - 对于给定的行,MSE 是否“太高”,表明该行中的数据实例是异常的:

# h2o.anomaly computes the per-row reconstruction error for the test data set
# (passing it through the autoencoder model and computing mean square error (MSE) for each row)
test_rec_error <- as.data.frame(h2o.anomaly(ae_model, test_hex)) 

为了演示的目的,让我们更进一步。下面的代码将显示与低和高重建错误相关的数字:

# let's look at the test set points with low/high reconstruction errors.

# We will now visualize the original test set points to see the digits with 

# the best and worst reconstruction error.



#get the ascending indexes into the data based on error

row_idx <- order(test_rec_error[,1],decreasing=F)


#pull the recons back into r so we can index them

test_r <- as.data.frame(test_hex)


# The good

# Let's plot the 25 digits with lowest reconstruction error.

plotDigits(test_r,   test_rec_error, row_idx[1:25])




Screen Shot 2022-06-13 at 9.23.03 AM.png


# The ugly
# And here are the biggest outliers - The 25 digits with highest reconstruction error!
plotDigits(test_r,   test_rec_error, row_idx[9976:10000])


Screen Shot 2022-06-13 at 9.25.20 AM.png

由此我们可以看出,最低的重构 MSE 来自数字“1”的写得很好的例子——最简单的数字。最高的 MSE 来自稍微不规则书写的更复杂的数字——“8”、“4”、“6”和“2”。

具有最高变化的数字位于具有高重构误差的数字集中是有道理的。在我的下一篇博文中,我将向您展示如何将此框架应用到更真实的世界——Apache Web 服务器日志访问文件。

关于结果的注释,如果在多个内核上运行,则 H2O 的结果不可重现。每次训练深度学习模型时,结果和性能指标可能会略有不同。H2O 中的实现使用了一种称为“Hogwild!”的技术。这以牺牲多核的可重复性为代价提高了训练速度。如果你想要可重现的结果,你需要限制 H2O 在单核上运行,并确保在调用h2o.deeplearning和设置中使用种子reproducible=TRUE

异常检测入门

MNIST 是一个著名的标准化数据集,用于分类模型开发和测试以及异常检测开发和测试。有多种方法可以在数据中执行异常检测,并且自动编码器经常使用并且相当容易理解。在这个过程中,训练样例的输入向量也被用作训练的ground truth,结果是训练模型学习训练样例的识别函数。之后,该模型可以针对新数据运行以确定它与多维输入向量空间中的训练数据的“接近程度”——MSE 的大小表示“正常”和“异常”。

这篇博文中重点介绍的技术——R、H20 和 MinIO——构成了一个强大而快速的 ML 工具箱。本教程提供了一个异常检测示例,并提供了大量理论知识——在后续博客文章关于 Apache 访问日志异常检测之前学习所有好东西。

立即下载 MinIO并构建您的 ML 工具包。如果您有任何问题,请发送电子邮件至 sales@minio.org.cn,或加入MinIO slack 频道并提问。


上一篇 下一篇