新奇性和异常值检测

在我的前几篇文章中,我研究了许多机器学习可以帮助进行预测的方法。基本思想是,您使用现有数据创建一个模型,然后要求该模型根据新数据预测结果。

因此,机器学习最令人惊叹的应用之一是预测未来也就不足为奇了。就在撰写本文的前几天,有人宣布机器学习模型实际上可能能够预测地震——这是一个科学家多年来一直未能实现的目标,并且有可能拯救成千上万,甚至数百万人的生命。

但正如您所看到的,机器学习也可以用于“聚类”数据——即找到人类无法或不愿看到的模式,并尝试将数据放入各种“集群”或机器驱动的类别中。通过要求计算机将数据划分为不同的组,您就有机会发现和利用以前未被检测到的模式。

正如聚类可以用来将数据划分为许多连贯的组一样,它也可以用来决定哪些数据点属于组内,哪些不属于。在“新奇性检测”中,您有一个仅包含良好数据的数据集,并且您正在尝试确定新的观测结果是否适合现有数据集。在“异常值检测”中,数据可能包含异常值,您想要识别它们。

这种检测可以在哪里有用?考虑一下您可以使用这样的系统回答的几个问题

  • 来自特定 IP 地址的异常登录尝试次数是否过多?

  • 是否有任何客户在给定时间内购买的产品数量超过典型数量?

  • 在干旱期间,哪些家庭的用水量高于平均水平?

  • 哪些法官判决的被告人数异常之多?

  • 患者的血液检查应该被认为是正常的,还是存在需要进一步检查和检验的异常值?

在所有这些情况下,您可以设置最小值和最大值阈值,然后告诉计算机使用这些阈值来确定什么是可疑的。但是机器学习改变了这一点,让计算机弄清楚什么是被认为是“正常”的,然后识别异常,然后人类可以调查这些异常。这使人们可以将精力集中在理解异常值是否确实有问题,而不是首先识别它们。

因此,在本文中,我将介绍一些您可以使用 Python 提供的用于处理数据的工具和库(NumPy、Pandas 和 scikit-learn)来尝试识别异常值的方法。哪种技术和工具适合您的数据取决于您正在做什么,但是这里介绍的基本理论和实践至少应该为您提供一些思考的素材。

查找异常

人类非常擅长发现模式,他们也很擅长发现不符合模式的事物。但是,什么样的算法可以查看一组数据集并找出哪个与其他数据集不同呢?

一种简单的方法是设置一个截止值,通常设置为一个或两个标准差。对于那些没有统计学背景(或已经忘记“标准差”是什么)的人来说,它是衡量数据分散程度的指标。例如


>>> a = np.array([10,10,10,10,10,10,10])
>>> print("std = {}, mean = {}".format(a.std(), a.mean()))

std = 0.0, mean = 10.0

在上面的例子中,我有一个 NumPy 数组,其中包含七个数字 10 的实例。人们通常认为平均值描述了数据,它确实如此,但只有当它与标准差结合使用时,您才能知道数字彼此之间的差异有多大。在本例中,它们都是相同的,因此标准差为 0。

在这个例子中,平均值保持不变,但标准差却大相径庭


>>> a = np.array([5,15,0,20,-5,25,10])
>>> print("std = {}, mean = {}".format(a.std(), a.mean()))

std = 10.0, mean = 10.0

在这里,平均值没有改变,但标准差改变了。您可以从这两个数字中看出,尽管数字仍然以 10 为中心,但它们也分布得很广。

检测异常数据的一种简单方法是查找所有位于距平均值两个标准差之外的值,这约占数据的 95%。(如果您愿意,您可以进一步扩展;99.73% 的数据点在三个标准差之内,99.994% 在四个标准差之内。)如果您正在现有数据集中查找异常值,您可以执行以下操作


>>> a = np.array([-5,15,0,20,-5,25,1000])
>>> print(a.std())

347.19282415231044

>>> min_cutoff = a.mean() - a.std()*2
>>> max_cutoff = a.mean() + a.std()*2

>>> print(a[(a<min_cutoff) | (a>max_cutoff)])

array([1000])

果然,它找到了数据中的一个异常值。

如果您有一堆新数据并想确定这些值是否适合您的现有数据集内部或外部,那就更容易了


>>> new_data = np.array([-5000, -3000, -1000, -500, 20, 60, 500, 800,
>>> 900])
>>> print(new_data[(new_data<min_cutoff) | (new_data>max_cutoff)])

array([-5000, -3000, -1000,   900])

好消息是这很简单——易于理解、易于实施且易于自动化。

但是,对于大多数数据来说,这也太简单了。您不太可能查看一维向量。基线(平均值)可能会随着时间而移动。此外,一定有其他更好的方法来衡量某物是“内部”还是“外部”,对吧?

变得更复杂

对于现实世界的异常检测,您需要改进几个方面。您需要考虑数据并确定什么是“内部”和什么是“外部”。您还需要找到评估模型的方法。

让我们考虑新奇性检测:存在初始数据,您想知道新数据是否适合现有数据内部,或者是否会被视为异常值。例如,考虑一位带着血液检查值的病人来就诊。这些测试是否表明患者是正常的,因为数据的值与您已经看到的值相似?或者这些新值是统计异常值,表明患者需要额外关注?

为了尝试新奇性和异常值检测,我下载了宾夕法尼亚州(Wyncote)费城郊外地区 2016 年每天的历史降水数据。因为我是个科学型的人,所以我下载了公制单位的数据。这些数据来自美国政府。

该站点包含从此处下载数据的明确说明。

政府数据可以免费获得,以及一旦您检索到数据后可以进行的各种分析,这真是令人惊叹。

我将数据下载为 CSV 文件,然后使用 Pandas 将其读入数据框


>>> df = pd.read_csv('/Users/reuven/downloads/914914.csv',
    usecols=['PRCP', 'DATE'])

请注意,我只对 PRCP(降水)和 DATE(日期,YYYYMMDD 格式)感兴趣。然后,我操作数据以分解 DATE 列,然后将其删除


>>> df['DATE'] = df['DATE'].astype(np.str)
>>> df['MONTH'] = df['DATE'].str[4:6].astype(np.int8)
>>> df['DAY'] = df['DATE'].str[6:8].astype(np.int8)
>>> df.drop('DATE', inplace=True, axis=1)

我为什么要分解日期?因为模型可能更容易处理三个单独的数字列,而不是单个日期时间列。此外,将这些列作为模型的一部分将更容易理解七月份下雪是否异常。我忽略了年份,因为它对于每条记录都是相同的,这意味着它不能作为此模型中的预测因子来帮助我。

我的数据框现在包含来自 2016 年的 353 行数据(我不确定为什么不是 365 行),其中包含指示降雨量(毫米)、日期和月份的列。

基于此,您如何构建一个模型来指示给定日期的降雨量是正常还是异常值?

在 scikit-learn 中,您始终使用相同的方法:您导入估计器类,创建该类的实例,然后拟合模型。在监督学习的情况下,“拟合”意味着教导模型哪些输入与哪些输出相关联。在我在这里进行的无监督学习的情况下,您仅使用一组输入来“拟合”,从而允许模型区分内部值和异常值。

创建模型

对于此数据,我可以构建几种类型的模型。我做了一些实验,发现 IsolationForest 估计器给了我最好的结果。以下是我创建和训练模型的方式


>>> from sklearn.ensemble import IsolationForest
>>> model = IsolationForest()
>>> model.fit(df)

模型现在已经训练好了,所以我可以找出给定月份和日期的特定降雨量是否被认为是正常的。

为了试用一下,我针对模型自身的输入检查模型


>>> Series(model.predict(df)).value_counts()

在上面的代码中,我运行 model.predict(df)。这给出了模型的输入,并要求它预测这些是正常、预期值(用 1 表示)还是异常值(用 –1 表示)。通过将结果转换为 Pandas 系列,然后调用 value_counts,我看到


 1    317
-1     36

尽管它错误地将 36 天标记为异常值,但也许那些日子确实不寻常。如果模型拥有多年的数据,而不是仅有一年的数据,那么模型肯定会得到改进。

现在怎么办?我可以要求系统进行一些预测


for i in range(1, 13):
    print(model.predict([[15, i, 16]]))

这将告诉您每个月 16 日获得 15 毫米降雨量是否正常。模型的结论:是的,在二月到七月是完全正常的,但在八月到一月则不然。如果零降水呢


for i in range(1, 13):
    print(model.predict([[0, i, 16]]))

事实证明,无论哪个月份,在每月 16 日零降雨量永远不是异常值。

当然,这些只是粗略的测试。真正要做的是使用我们的老朋友 train_test_split


>>> from sklearn.model_selection import train_test_split
>>> X_train, X_test = train_test_split(df)
>>> model.fit(X_train)
>>> Series(model.predict(X_test)).value_counts()

考虑到我甚至没有尝试调整它,该模型做得相当不错


 1    77
-1    12
dtype: int64

换句话说,给定应该全部归类为内部值的数据,您可以在此处看到绝大多数确实被正确分类。

还有其他类型的估计器可以使用。特别是,One-Class SVM 估计器在处理输入数据方面有着良好的记录。如果再加上更大的数据集,很可能会改善上面显示的结果——尽管在为本文尝试 One-Class SVM 时,我没有看到任何这样的结果。如果我添加更多年的数据,其他估计器可能会效果更好。

结论

新奇性和异常值检测是机器学习(又一个)广泛、令人兴奋且不断增长的用途。与机器学习一样,问题不在于编码,而在于将数据整理成您可以使用的格式,然后调整模型定义,直到找到一个以高度置信度预测或识别异常值的模型。

资源

我在本文中使用了 Python 和 SciPy 堆栈的许多部分(NumPy、SciPy、Pandas、Matplotlib 和 scikit-learn)。所有这些都可以从 PyPISciPy.org 获得。

scikit-learn 的文档有一些(但不多)关于新奇性/异常值检测的文档

PyPI 和 GitHub 上提供了一个用于检测异常的简单 Python 包 lsanomaly。对于简单的数据集,它可能值得考虑。

正如我之前提到的,美国政府 NOAA(国家海洋和大气管理局)网站包含大量天气和气候数据,您可以免费下载

Reuven Lerner 在世界各地的公司教授 Python、数据科学和 Git。您可以订阅他免费的每周“更好的开发者”电子邮件列表,并从他的书籍和课程中学习,网址为 http://lerner.co.il。Reuven 与他的妻子和孩子们住在以色列的莫迪因。

加载 Disqus 评论