# 5.22 无监督学习：异常检测

常检测是一种机器学习任务，包括发现所谓的异常值。

“异常值是一种数据集中的观测值，似乎与该组数据的其余部分不一致。”-- Johnson 1992

“异常值是一种观测值，与其他观测值有很大差异，引起人们怀疑它是由不同的机制产生的。”-- Outlier/Anomaly Hawkins 1980

## 异常检测设定的类型

+   监督 AD
    +   标签可用于正常和异常数据
    +   类似于稀有类挖掘/不平衡分类
+   半监督 AD（新奇检测）
    +   只有正常的数据可供训练
    +   该算法仅学习正常数据
+   无监督 AD（异常值检测）
    +   没有标签，训练集 = 正常 + 异常数据
    +   假设：异常非常罕见

```py
%matplotlib inline

import warnings
warnings.filterwarnings("ignore")

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
```

让我们首先熟悉不同的无监督异常检测方法和算法。 为了可视化不同算法的输出，我们考虑包含二维高斯混合的玩具数据集。

## 生成数据集

```py
from sklearn.datasets import make_blobs

X, y = make_blobs(n_features=2, centers=3, n_samples=500,
                  random_state=42)

X.shape

plt.figure()
plt.scatter(X[:, 0], X[:, 1])
plt.show()
```

## 使用密度估计的异常检测

```py
from sklearn.neighbors.kde import KernelDensity

# 用高斯核密度估计器估算密度
kde = KernelDensity(kernel='gaussian')
kde = kde.fit(X)
kde

kde_X = kde.score_samples(X)
print(kde_X.shape)  # 包含数据的对数似然。 越小样本越罕见

from scipy.stats.mstats import mquantiles
alpha_set = 0.95
tau_kde = mquantiles(kde_X, 1. - alpha_set)

n_samples, n_features = X.shape
X_range = np.zeros((n_features, 2))
X_range[:, 0] = np.min(X, axis=0) - 1.
X_range[:, 1] = np.max(X, axis=0) + 1.

h = 0.1  # step size of the mesh
x_min, x_max = X_range[0]
y_min, y_max = X_range[1]
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                     np.arange(y_min, y_max, h))

grid = np.c_[xx.ravel(), yy.ravel()]

Z_kde = kde.score_samples(grid)
Z_kde = Z_kde.reshape(xx.shape)

plt.figure()
c_0 = plt.contour(xx, yy, Z_kde, levels=tau_kde, colors='red', linewidths=3)
plt.clabel(c_0, inline=1, fontsize=15, fmt={tau_kde[0]: str(alpha_set)})
plt.scatter(X[:, 0], X[:, 1])
plt.show()
```

## 单类 SVM

基于密度的估计的问题在于，当数据的维数增加时，它们往往变得低效。 这就是所谓的维度灾难，尤其会影响密度估算算法。 在这种情况下可以使用单类 SVM 算法。

```py
from sklearn.svm import OneClassSVM

nu = 0.05  # theory says it should be an upper bound of the fraction of outliers
ocsvm = OneClassSVM(kernel='rbf', gamma=0.05, nu=nu)
ocsvm.fit(X)

X_outliers = X[ocsvm.predict(X) == -1]

Z_ocsvm = ocsvm.decision_function(grid)
Z_ocsvm = Z_ocsvm.reshape(xx.shape)

plt.figure()
c_0 = plt.contour(xx, yy, Z_ocsvm, levels=[0], colors='red', linewidths=3)
plt.clabel(c_0, inline=1, fontsize=15, fmt={0: str(alpha_set)})
plt.scatter(X[:, 0], X[:, 1])
plt.scatter(X_outliers[:, 0], X_outliers[:, 1], color='red')
plt.show()
```

## 支持向量 - 离群点

所谓的单类 SVM 的支持向量形成离群点。

```py
X_SV = X[ocsvm.support_]
n_SV = len(X_SV)
n_outliers = len(X_outliers)

print('{0:.2f} <= {1:.2f} <= {2:.2f}?'.format(1./n_samples*n_outliers, nu, 1./n_samples*n_SV))
```

只有支持向量涉及单类 SVM 的决策函数。

+   绘制单类 SVM 决策函数的级别集，就像我们对真实密度所做的那样。
+   突出支持向量。

```py
plt.figure()
plt.contourf(xx, yy, Z_ocsvm, 10, cmap=plt.cm.Blues_r)
plt.scatter(X[:, 0], X[:, 1], s=1.)
plt.scatter(X_SV[:, 0], X_SV[:, 1], color='orange')
plt.show()
```

> 练习
> 
> 更改`gamma``参数并查看它对决策函数平滑度的影响。

```py
# %load solutions/22_A-anomaly_ocsvm_gamma.py
```

## 隔离森林

隔离森林是一种基于树的异常检测算法。 该算法构建了许多随机树，其基本原理是，如果样本被隔离，在非常少量的随机分割之后，它应该单独存在于叶子中。 隔离森林根据样本最终所在的树的深度建立异常得分。

```py
from sklearn.ensemble import IsolationForest

iforest = IsolationForest(n_estimators=300, contamination=0.10)
iforest = iforest.fit(X)

Z_iforest = iforest.decision_function(grid)
Z_iforest = Z_iforest.reshape(xx.shape)

plt.figure()
c_0 = plt.contour(xx, yy, Z_iforest,
                  levels=[iforest.threshold_],
                  colors='red', linewidths=3)
plt.clabel(c_0, inline=1, fontsize=15,
           fmt={iforest.threshold_: str(alpha_set)})
plt.scatter(X[:, 0], X[:, 1], s=1.)
plt.show()
```

> 练习
> 
> 以图形方式说明树的数量对决策函数平滑度的影响。

```py
# %load solutions/22_B-anomaly_iforest_n_trees.py
```

## 数字数据集上的图解

我们现在将应用`IsolationForest`算法来查找以非常规方式编写的数字。

```py
from sklearn.datasets import load_digits
digits = load_digits()
```

数字数据集包括`8×8`的数字图像。

```py
images = digits.images
labels = digits.target
images.shape

i = 102

plt.figure(figsize=(2, 2))
plt.title('{0}'.format(labels[i]))
plt.axis('off')
plt.imshow(images[i], cmap=plt.cm.gray_r, interpolation='nearest')
plt.show()
```

要将图像用作训练集，我们需要将图像展开。

```py
n_samples = len(digits.images)
data = digits.images.reshape((n_samples, -1))

data.shape

X = data
y = digits.target

X.shape
```

让我们关注数字 5。

```py
X_5 = X[y == 5]

X_5.shape

fig, axes = plt.subplots(1, 5, figsize=(10, 4))
for ax, x in zip(axes, X_5[:5]):
    img = x.reshape(8, 8)
    ax.imshow(img, cmap=plt.cm.gray_r, interpolation='nearest')
    ax.axis('off')
```

+   让我们使用`IsolationForest`来查找前 5% 最异常的图像。
+   让我们绘制他们吧！

```py
from sklearn.ensemble import IsolationForest
iforest = IsolationForest(contamination=0.05)
iforest = iforest.fit(X_5)
```

使用`iforest.decision_function`计算“异常”的级别。越低就越异常。

```py
iforest_X = iforest.decision_function(X_5)
plt.hist(iforest_X);
```
让我们绘制最强的正常值。

```py
X_strong_inliers = X_5[np.argsort(iforest_X)[-10:]]

fig, axes = plt.subplots(2, 5, figsize=(10, 5))

for i, ax in zip(range(len(X_strong_inliers)), axes.ravel()):
    ax.imshow(X_strong_inliers[i].reshape((8, 8)),
               cmap=plt.cm.gray_r, interpolation='nearest')
    ax.axis('off')
```

让我们绘制最强的异常值。

```py
fig, axes = plt.subplots(2, 5, figsize=(10, 5))

X_outliers = X_5[iforest.predict(X_5) == -1]

for i, ax in zip(range(len(X_outliers)), axes.ravel()):
    ax.imshow(X_outliers[i].reshape((8, 8)),
               cmap=plt.cm.gray_r, interpolation='nearest')
    ax.axis('off')
```

> 练习
> 
> 用所有其他数字重新运行相同的分析。

```py
# %load solutions/22_C-anomaly_digits.py
```
