异常检测

本节定位
异常检测用于发现数据中**"不正常"的样本**——信用卡欺诈、网络入侵、设备故障等。与分类不同,异常通常是稀有的,没有足够的标签,所以需要特殊的算法。
学习目标
- 掌握统计方法异常检测(Z-score、IQR)
- 掌握 Isolation Forest
- 了解 One-Class SVM
- 了解 LOF(局部离群因子)
- 能对比选择合适的方法
先说一个很重要的学习预期
这一节对新人最容易卡住的地方,是它不像普通分类那样有很清楚的标签和边界。
更适合第一遍先学会的,不是把每个方法都背熟,而是先建立这条线:
异常检测很多时候是在先定义“正常长什么样”,再判断谁偏离得太远。
只要这条线立住,后面的统计方法、Isolation Forest、LOF 和阈值选择就会顺很多。
先建立一张地图
异常检测对新人来说最难的地方在于:
它不像普通分类那样“标签很清楚,直接学就行”。很多时候你面对的是:
- 异常非常少
- 异常标签不完整
- 新异常长得和旧异常还不一样
更稳的理解顺序是:
所以异常检测最重要的不是先记模型名,而是先把“你眼里的异常到底是什么”想清楚。
一、异常检测概述
1.1 什么是异常?
异常(Anomaly / Outlier)= 与大多数数据显著不同的样本。
| 应用场景 | 正常数据 | 异常数据 |
|---|---|---|
| 信用卡欺诈 | 正常消费 | 盗刷交易 |
| 工业监控 | 设备正常运行 | 设备即将故障 |
| 网络安全 | 正常流量 | DDoS 攻击 |
| 医疗诊断 | 健康指标正常 | 疾病信号 |
1.2 为什么不用分类器?
| 问题 | 说明 |
|---|---|
| 数据不平衡 | 异常样本可能只有 0.1% |
| 缺乏标签 | 很多异常事先不知道长什么样 |
| 异常多变 | 新型欺诈手段不断出现 |
1.2.1 异常检测和分类真正差在哪?
分类通常是在学:
- “A 和 B 怎么分”
而异常检测更常见的是在学:
- “大多数正常样本长什么样”
- “偏离到什么程度该开始警觉”
这也是为什么异常检测里,阈值、误报、漏报和业务代价会格外重要。
1.2.2 一个更适合新人的类比
你可以先把异常检测想成:
- 先理解“正常人的日常节奏”
比如:
- 正常设备的温度波动大概在哪
- 正常用户的登录时间和操作频率大概在哪
这时异常并不一定是“完全没见过”,
而是:
- 偏离正常模式偏得太远
- 或者落在很少有人会去的位置上
1.3 生成演示数据
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
# 正常数据:二维高斯分布
n_normal = 300
X_normal = np.random.randn(n_normal, 2) * 1.5 + [5, 5]
# 异常数据:随机散布
n_anomaly = 15
X_anomaly = np.random.uniform(0, 12, (n_anomaly, 2))
# 合并
X_all = np.vstack([X_normal, X_anomaly])
y_true = np.array([1] * n_normal + [-1] * n_anomaly) # 1=正常, -1=异常
plt.figure(figsize=(8, 6))
plt.scatter(X_normal[:, 0], X_normal[:, 1], s=20, alpha=0.6, label='正常', color='steelblue')
plt.scatter(X_anomaly[:, 0], X_anomaly[:, 1], s=80, marker='x', color='red',
linewidths=2, label='异常')
plt.title('异常检测演示数据')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
二、统计方法
2.1 Z-score 方法
原理:假设数据服从正态分布,距离均值超过 N 个标准差的样本视为异常。
Z = (x - μ) / σ
通常 |Z| > 3 被视为异常(99.7% 的数据在 3σ 以内)。
from scipy import stats
# 一维示例
data_1d = np.concatenate([np.random.randn(200) * 2 + 10, [25, -5, 30]])
z_scores = np.abs(stats.zscore(data_1d))
threshold = 3
anomalies = z_scores > threshold
plt.figure(figsize=(10, 4))
plt.scatter(range(len(data_1d)), data_1d, c=['red' if a else 'steelblue' for a in anomalies],
s=20, alpha=0.7)
plt.axhline(y=data_1d[~anomalies].mean() + threshold * data_1d[~anomalies].std(),
color='orange', linestyle='--', label='+3σ')
plt.axhline(y=data_1d[~anomalies].mean() - threshold * data_1d[~anomalies].std(),
color='orange', linestyle='--', label='-3σ')
plt.title(f'Z-score 异常检测( 发现 {anomalies.sum()} 个异常)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
2.2 IQR 方法
原理:基于四分位数,不依赖正态分布假设。
- IQR = Q3 - Q1(四分位距)
- 异常:x < Q1 - 1.5×IQR 或 x > Q3 + 1.5×IQR
# IQR 方法
Q1 = np.percentile(data_1d, 25)
Q3 = np.percentile(data_1d, 75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
anomalies_iqr = (data_1d < lower) | (data_1d > upper)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 箱线图
axes[0].boxplot(data_1d, vert=False)
axes[0].set_title('箱线图(自动标记异常值)')
# 散点图
axes[1].scatter(range(len(data_1d)), data_1d,
c=['red' if a else 'steelblue' for a in anomalies_iqr], s=20, alpha=0.7)
axes[1].axhline(y=upper, color='orange', linestyle='--', label=f'上界={upper:.1f}')
axes[1].axhline(y=lower, color='orange', linestyle='--', label=f'下界={lower:.1f}')
axes[1].set_title(f'IQR 异常检测(发现 {anomalies_iqr.sum()} 个异常)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
2.3 Z-score vs IQR
| Z-score | IQR | |
|---|---|---|
| 假设 | 正态分布 | 无分布假设 |
| 鲁棒性 | 受极端值影响 | 更鲁棒 |
| 适用 | 大致正态的数据 | 任何分布 |
| 阈值 | 通常 3σ | 1.5×IQR |
2.4 统计方法什么时候还特别值得先试?
如果你现在遇到的是:
- 低维数据
- 规则很简单
- 只是想先快速发现明显离群点
那统计方法仍然非常值得先试。
它的价值不只是“简单”,还在于:
- 你能很快得到一个可解释 baseline
- 你能先大致感受异常比例
- 你能更早发现数据分布本身的问题
三、Isolation Forest
3.1 原理
Isolation Forest(孤立森林)的思路非常巧妙:
异常点更容易被"孤立"——只需很少的分割就能把它隔开。
| 概念 | 说明 |
|---|---|
| 路径长度 | 从根节点到叶节点的步数 |
| 异常分数 | 路径越短 → 分数越高 → 越可能是异常 |
| 正常点 | 被"正常"数据包围,需要更多次分割才能孤立 |
3.2 sklearn 实现
from sklearn.ensemble import IsolationForest
# 训练 Isolation Forest
iso = IsolationForest(
n_estimators=100,
contamination=0.05, # 预估异常比例
random_state=42
)
y_pred_iso = iso.fit_predict(X_all) # 1=正常, -1=异常
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 预测结果
colors_pred = ['red' if p == -1 else 'steelblue' for p in y_pred_iso]
axes[0].scatter(X_all[:, 0], X_all[:, 1], c=colors_pred, s=20, alpha=0.7)
n_detected = (y_pred_iso == -1).sum()
axes[0].set_title(f'Isolation Forest 检测结果\n(检测到 {n_detected} 个异常)')
# 异常分数热图
xx, yy = np.meshgrid(np.linspace(-2, 14, 200), np.linspace(-2, 14, 200))
Z = iso.decision_function(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
axes[1].contourf(xx, yy, Z, levels=20, cmap='RdBu')
axes[1].scatter(X_all[:, 0], X_all[:, 1], c=colors_pred, s=20, edgecolors='white', linewidth=0.5)
axes[1].set_title('异常分数热图\n(蓝=正常,红=异常)')
for ax in axes:
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 评估
from sklearn.metrics import classification_report
print("Isolation Forest 评估:")
print(classification_report(y_true, y_pred_iso, target_names=['异常(-1)', '正常(1)']))
3.3 关键参数
| 参数 | 说明 | 推荐 |
|---|---|---|
n_estimators | 树的数量 | 100(默认) |
contamination | 异常比例的估计 | 根据业务设定 |
max_samples | 每棵树的采样数 | 'auto' 或 256 |
max_features | 每棵树使用的特征比例 | 1.0(默认) |
3.4 为什么 Isolation Forest 常常是第一选择?
因为它在很多真实任务里,正好卡在一个很实用的平衡点上:
- 比统计方法更能处理高维数据
- 比 One-Class SVM 更容易扩展到更大数据
- 比 LOF 更适合作为通用 baseline
所以第一次做异常检测项目时,如果你还没有特别明确的结构假设,先试 Isolation Forest 往往是最稳的。