机器学习复习Day8——异常检测


type: note

title: 异常检测编程作业

date: 2026-08-29

description: C3W1 异常检测两步走:①estimate_gaussian 按列算 μ(均值)和 σ²,给正常数据套钟形罩子 ②select_threshold 用验证集 F1 挑阈值 ε,p<ε 判异常。核心 tp/fp/fn 只在"被判坏"里统计 tp+fp,prec 分母=判坏总数、rec 分母=真坏总数。不用 accuracy 挑 ε:异常极少时它虚高,会挑出"什么都不抓"的废物阈值。F1 是**调和平均**贴着小的走,两个极端(全判正常 F1=0、全判坏 F1≈3.9%)都低分。11 维 ε 小到 1.38e-18 是因为对角协方差=各维独立=11 个小于 1 的密度相乘;ε 绑定本数据集概率尺度,跨数据集搬必废。坑:fp/fn 套用时串行、F1 分母漏加 rec、μ 误记成中位数,实际为均值、误以为 p 分布会跟着 ε 变,实际不变。


2026-08-29 学习笔记

笔记区(学的时候随手记)

核心观点

**异常检测就干一件事:先学会「正常长什么样」,再把离谱的挑出来。**

场景:307 台服务器,每台记两个指标(延迟 ms、吞吐量 mb/s),**没有标签**,没人告诉 Claude 哪台坏了。做法分两步,正好对应两个 Exercise:

  1. **量出正常范围**(Ex1):算出每个指标的平均值 μ 和方差 σ²,等于给正常数据套一个钟形罩子。μ 说"正常大概在哪",σ² 说"正常允许晃多大"。

  2. **划一条「多低算低」的线**(Ex2):罩子会给每台机器算一个概率 p。p 特别小 = 落在罩子边缘 = 离谱。这条线叫 **ε**,用一小份**带标签的**验证集来挑。

> 训练集没标签,验证集有标签。验证集专门用来挑 ε。

关键方法/流程

**数据流(三步接力)**

```

X_train ──estimate_gaussian──> mu, var (量出正常范围)

X_val + mu/var ──multivariate_gaussian──> p_val (给每台验证机打概率)

y_val + p_val ──select_threshold──> epsilon, F1 (挑出最好的线)

```

  • **`estimate_gaussian(X)`** → 返回 `mu`、`var`,形状都是 `(n,)`,**每个特征一个数**,不是标量。必须按列算(`axis=0`),跟 K-means 里 `np.mean(..., axis=0)` 是同一个坑。

  • **`multivariate_gaussian(X, mu, var)`**→ 吐出每台机器的概率 p。看 `utils.py` 实现:传进去的 `var` 是一维向量时会走 `np.diag(var)` 摊成**对角**协方差矩阵,也就是**各维互相独立**,总概率 = 各维概率**相乘**。

  • **`select_threshold(y_val, p_val)`** → 外层循环已给(把 min(p) 到 max(p) 切成 1000 份,每份当一次候选 ε),要写的只有「当前这个 ε 的 F1 是多少」。

**accuracy 陷阱(为什么不用准确率挑 ε)**

异常天生是少数派。假设 200 台里只有 4 台真坏,写个「一律判正常」的废物模型:

  • accuracy = 196/200 = **98%**,看着漂亮

  • 但它一台坏机器都没抓到 → tp = 0 → **F1 = 0**

**accuracy 奖励多数派**,所以在异常检测里它奖励「什么都别抓」。拿 accuracy 挑 ε,会一路把 ε 压到极小(判坏的越少 → accuracy 越高),最后挑出「一台不判坏」这个废物阈值。F1 里 rec 的分母是「真坏的总台数」,抓不到就是 0 分,一上来就把这种阈值判死。

**F1 是调和平均,贴着小的那个走**

两个极端摆一起看(200 台、真坏 4 台):

| 模型 | tp | fp | fn | prec | rec | F1 |

| ---------- | --- | --- | --- | ----------- | --- | -------------------------- |

| 一律判正常 | 0 | 0 | 4 | 0/0 = `nan` | 0 | `nan`(等于 0 分,被淘汰) |

| 一律判坏 | 4 | 196 | 0 | 0.02 | 1 | 0.04/1.02 ≈ **0.039** |

「一律判坏」recall 满分 1(一个没漏),可 prec 只有 2%(冤枉 196 台),F1 被拽到 3.9%。**一边烂,整体就烂**------这就是 F1 防偏科的机制。数学原因:F1 是 prec 和 rec 的调和平均,天生贴着**小的那个**走。

  • `8.99e-05` 搬到 11 特征上 → 这条线比那边几乎所有 p 高 12 个数量级 → **1000 台几乎全判异常**(落回"一律判坏")

  • `1.38e-18` 搬到 2 特征上 → 这条线比那边几乎所有 p 低 16 个数量级 → **一台都不判异常**(落回"一律判正常")

另外:11 维 F1 只有 0.615,比 2 维的 0.875 差不少。**特征多不等于好抓。**

实战记录

**① `&` 优先级与括号**

> & 优先级比 == 高,所以必须写 (predictions == 1) & (y_val == 1),否则会被解析成 predictions == (1 & y_val) == 1,完全乱套。口诀:先各自比较,再取交集,所以一定要加括号。

```

1 & y_val 先算出来是: 1 1 0 0 ← 等于 y_val 本身

不加括号 -> ValueError: The truth value of an array with more than

one element is ambiguous. Use a.any() or a.all()

加括号正确结果 tp = 1

```

`1 & y_val` 先算成 `y_val` 本身 → 变成链式比较 `predictions == y_val == 1` → Python 对数组做链式比较要取真值 → 抛 ambiguous。**结论是报错,成因正是「完全乱套」。**

**② fn / fp 别写反**

> fn = 预测0 且 真值1(漏报)

> fp = 预测1 且 真值0(误报)

**③ 验证集是什么**

> val = validation(验证集),X_val + y_val 用来选阈值 ε,相当于"模拟考试"。

> 0 = 正常,1 = 异常。

**④ 布尔直接当 0/1(原话)**

> 比较运算本身就是布尔:p_val < epsilon 返回 True/False,而 True=1、False=0,所以 predictions 直接就是一串 0/1,不需要 if。

**⑤ `np.sum()` 与内置 `sum()`(原话)**

> np.sum() 和 sum() 效果一样,但用 np.sum() 更规范。

补一句适用范围:**今天这场合(一维布尔数组求个数)两者结果确实一样**,实测都得 2。换二维就分岔------`sum(X)` 得 `4 6`(把每一行当一个元素累加),`np.sum(X)` 得 `10`(全部加成标量),而且内置 `sum()` 不认 `axis` 参数,所以 Ex1 里必须用 `np.sum(..., axis=0)`。

**⑥ F1 是干什么的(原话)**

> F1 在准确率和召回率之间找平衡的一个"综合分",它逼着你在"找得准"和"找得全"之间不能偏科。准确率和召回率两手抓,最后我们要的就是准确高召回也高的模型

术语校准:`prec` 中文是**精确率**(查准率),不是「准确率」。「准确率」= accuracy = 判对的总数 ÷ 全部,是另一个指标,也正是**不能**用来挑 ε 的那个。这个区分在异常检测里要命,见上面「accuracy 陷阱」。

**⑦ 今天卡住的四处(踩坑记录)**

  1. **fp / fn 套用时串行**:定义(第 ④ 条)写得完全正确,但套到「一台都没判坏」这个前提上时答成了 `fp=4, fn=0`(正确是 `fp=0, fn=4`)。抓手:**tp 和 fp 都只在「被判为坏」的机器里统计**,判坏的是 0 台,这两个必须同时为 0;那 4 台真异常被判成了「正常」,全落进 fn。

  2. **F1 分母漏加 rec**:算「一律判坏」的 F1 时把分母写成 `2%`,只放了 `prec`、漏掉 `rec = 1`,得出 F1 = 2。「算出 2 不可能」------**F1 上限就是 1**,超过 1 必然算错。正确:分母 = `prec + rec` = 0.02 + 1 = **1.02**,F1 = 0.04/1.02 ≈ 0.039。

  3. **μ 误记成中位数**:μ 是**均值**(所有数加起来除以个数,就是 Ex1 那行代码);中位数是排序后正中间那个。印象有来源------正态分布左右对称,理论上均值、中位数、众数**三个重合在钟形最高点**,数值上确实撞一起。但定义两回事:换成不对称数据(如收入,少数极高值),均值被拽走、中位数几乎不动。作业里算的是均值。

  4. **误以为 p 的分布会跟着 ε 变**:`p(x)` 的分布(含中位数 4.01e-17)由**数据本身 + Ex1 算出的 μ/σ²** 决定,`multivariate_gaussian` 跑完就固定了,在挑 ε 之前就已经在那。ε 只是事后在这堆已定的数上**划的一条线**。比喻:p 的分布是一栋楼里所有人的身高,ε 是门框高度,换门框高度楼里人的身高一点没动。**正因为不跟着变,搬 ε 才会废。**

代码逐段展开

Ex1 `estimate_gaussian(X)` --- 量出「正常范围」

```python

def estimate_gaussian(X):

m, n = X.shape

mu = np.sum(X, axis = 0)/m # 每列求和 ÷ 行数 = 每个特征的平均值

var = np.sum((X - mu)**2, axis = 0)/m # 每个数减本列均值、平方、每列求平均 = 方差

return mu, var

```

  • `X.shape` 拆成 `m, n`:`m` = 样本数(307 台机器),`n` = 特征数(2 个指标)。

  • **`mu = np.sum(X, axis=0)/m`**:对应公式 μᵢ = (1/m)Σⱼ xᵢ⁽ʲ⁾。`axis=0` 是「沿行方向往下加」,得到每一**列**的和,再除以行数 m → 每个特征一个平均值,形状 `(2,)`。

  • 跟 `np.mean(X, axis=0)` 完全等价,这个写法更贴公式。

  • 不写 `axis=0` 会把整个数组揉成一个标量(全局平均),罩子直接废。

  • **`var = np.sum((X - mu)**2, axis=0)/m`\*\*:对应公式 σᵢ² = (1/m)Σⱼ (xᵢ⁽ʲ⁾ − μᵢ)²。

  • `X - mu`:`X` 是 (307, 2),`mu` 是 (2,),numpy 自动把 `mu` 这两个数按列对齐、铺到 307 行上逐行去减(**广播**),不用写循环。

  • `**2` 平方:让偏高和偏低都算「偏」,不互相抵消。

  • 再 `axis=0` 求每列平均 → 每个特征允许晃多大。

Ex2 `select_threshold(y_val, p_val)` --- 划「多低算低」那条线

```python

for epsilon in np.arange(min(p_val), max(p_val), step_size): # 外层循环作业已给

predictions = (p_val < epsilon) # 概率低于线 → 判为异常

tp = np.sum((predictions == 1) & (y_val == 1)) # 判坏 且 真坏 = 抓对

fp = np.sum((predictions == 1) & (y_val == 0)) # 判坏 但 正常 = 冤枉(误报)

fn = np.sum((predictions == 0) & (y_val == 1)) # 判正常 但 真坏 = 漏掉(漏报)

prec = tp/(tp + fp) # 抓的里面多少真坏

rec = tp/(tp + fn) # 真坏的抓到多少

F1 = 2*prec*rec/(prec + rec) # 准和全的折中分

if F1 > best_F1: # 这段作业已给,自动记住最好的

best_F1 = F1

best_epsilon = epsilon

```

  • **`predictions = (p_val < epsilon)`**:得到一串 True/False,长度 = 验证集机器数。`True` 就是 1、`False` 就是 0,**不需要 `if`**。

  • **`np.sum((条件A) & (条件B))`**:`&` 是逐元素「与」,两个条件**各自加括号**(`&` 优先级比 `==` 高,不加会抛 ambiguous,见实战记录第 ③ 条)。布尔数组里 True 当 1,求和就是个数------**不用写 for 遍历样本**。

  • **`prec` 与 `rec` 一劳永逸的记法------看分母是谁**:

  • `prec = tp/(tp+fp)`,分母 = **你判坏的总台数**

  • `rec = tp/(tp+fn)`,分母 = **真的坏的总台数**

  • **`F1 = 2*prec*rec/(prec+rec)`**:分母是**两个相加**,上限 1,算出大于 1 必然错。

  • **那条 `RuntimeWarning: invalid value encountered in scalar divide`**:某些极端 ε 让 `tp+fp = 0`(一台都没判坏),`prec` 变成 `0/0` = `nan`。不影响结果------`nan > best_F1` 恒为 False,那条线自然被跳过。看到不用慌。

线索区(学完后合上材料,自问自答)

**Q: 一个「不管输入什么一律判正常」的模型,tp / fp / fn 各是多少?**

A: tp = 0、fp = 0、fn = 全部真异常数。关键:**tp 和 fp 都只在「被判为坏」的机器里统计**,判坏的是 0 台,这两个必须同时为 0;所有真异常都被判成「正常」,全落进 fn。

**Q: 上面那个模型 prec、rec、F1 算出什么?**

A: `prec = 0/(0+0)` = `nan`(代码里那条 RuntimeWarning 就是它);`rec = 0/(0+真坏数)` = **0**,铁定的;F1 得 `nan`,循环里 `nan > best_F1` 恒 False,**这条线直接被淘汰,等于 0 分**。注意 prec 不是 0 而是 nan。

**Q: 为什么不能用 accuracy 挑 ε?**

A: 异常是少数派,accuracy 会被大量正常样本冲得虚高(200 台里 4 台坏,全判正常就有 98%)。**accuracy 奖励多数派**,用它挑会一路把 ε 压到极小、挑出「一台不判坏」的废物阈值。F1 里 rec 分母是真坏总数,抓不到就 0 分,糊弄不过去。

**Q: 「一律判坏」recall 满分 1,为什么 F1 还是很低?**

A: F1 是 prec 和 rec 的**调和平均,天生贴着小的那个走**。prec 只有 0.02(冤枉一大片),F1 = 2×0.02×1/(0.02+1) = 0.04/1.02 ≈ 0.039。一边烂整体就烂,这正是 F1 防偏科的作用。

**Q: 11 维数据的 ε 为什么小到 1.38e-18?**

A: `var` 一维 → `np.diag(var)` 对角协方差 → 各维独立 → 总概率 = 各维概率**相乘**。每维密度峰值只有 0.042~0.073(远小于 1),11 个相乘掉到 5.88e-15,实际点不会每维踩峰值,p 中位数进一步降到 4.01e-17。ε 落在中位数下一两个数量级,**位置合理**。

**Q: 能不能把一份数据挑出的 ε 搬到另一份用?**

A: 不能,两头都废。`8.99e-05` 搬到 11 特征上(p 中位数 4.01e-17)→ 比几乎所有 p 高 12 个数量级 → 全判异常;`1.38e-18` 搬到 2 特征上(p 中位数 6.51e-02)→ 比几乎所有 p 低 16 个数量级 → 一台不判。**ε 只是在本数据集自己的概率尺度上划的一条线**,特征越多整条尺度整体往下平移。

**Q: p(x) 的分布会不会跟着 ε 变?**

A: 不会。p 的分布由**数据 + Ex1 算出的 μ/σ²** 决定,`multivariate_gaussian` 跑完就固定,挑 ε 之前就在那了。ε 只是事后划的线。比喻:p 是楼里所有人的身高,ε 是门框高度,换门框身高不动。

**Q: μ 是中位数吗?**

A: 不是,μ 是**均值**(加总除以个数,就是 Ex1 那行代码)。正态分布左右对称,理论上均值、中位数、众数三个重合在钟形最高点,所以数值上会撞一起------但定义不同。换成不对称数据(如收入),均值被极端值拽走、中位数几乎不动。

**Q: Ex1 里为什么必须用 `np.sum(..., axis=0)`,不能用内置 `sum()`?**

A: 内置 `sum()` 不认 `axis` 参数。一维布尔数组上两者结果一样,但二维就分岔:`sum(X)` 把每一行当一个元素累加得 `4 6`,`np.sum(X)` 全部加成标量得 `10`。要「每列一个数」只能用 `np.sum(..., axis=0)`。


总结(50 字以内)

异常检测 = 套钟形罩子(μ、σ² 按列算)+ 用验证集 F1 挑线 ε。tp/fp 只在判坏里统计;prec 分母判坏数、rec 分母真坏数;不用 accuracy(少数派场景虚高)。ε 绑数据尺度,11 维相乘故小到 1e-18,不可搬。


复习卡片

| 概念 | 一句话 | 我的场景 |

| ----------------- | -------------------------------------- | -------------------------------- |

| 异常检测两步 | 量正常范围(μ、σ²)→ 划线 ε | p < ε 判异常 |

| estimate_gaussian | 每列均值 + 每列方差,`axis=0` | 形状 (n,) 不是标量 |

| 广播 | `X - mu`:(307,2) 减 (2,) 自动按列铺开 | 不用写循环 |

| tp / fp | 都只在**被判坏**里统计 | 判坏 0 台 → 两个都 0 |

| fn | 判正常但真坏 = 漏报 | 「一律判正常」时 fn = 全部真异常 |

| prec 分母 | **你判坏的总数** tp+fp | 0/0 = nan,那条 RuntimeWarning |

| rec 分母 | **真坏的总数** tp+fn | 抓不到就是 0 |

| F1 | 调和平均,贴着小的那个走,上限 1 | 分母 prec+rec 别漏项 |

| accuracy 陷阱 | 奖励多数派,异常少时虚高 | 全判正常 98% 但 F1=0 |

| 对角协方差 | `np.diag(var)` = 各维独立 = 概率相乘 | 11 维 ε 小到 1e-18 的原因 |

| ε 不可搬 | 只在本数据集概率尺度上有意义 | 搬过去全判坏,搬回来全不判 |

| μ | 均值,不是中位数 | 正态分布下两者数值重合但定义不同 |

| p 的分布 | 由数据 + μ/σ² 决定,与 ε 无关 | ε 是事后划的线,门框换了身高不动 |

相关推荐
TAN-90°-21 分钟前
Deep Learning for Computer Vision——Attention and Transformers
人工智能·深度学习·神经网络·机器学习·计算机视觉·cnn·机器翻译
千里码aicood21 分钟前
Django 基于Django的电脑推荐与分析可视化系统设计与实现
python·django·电脑
Profile排查笔记22 分钟前
指纹浏览器手机版怎么选?从本地 App 到云端 Android 的实现方式解析
前端·人工智能·后端·自动化
shiter26 分钟前
舍筏 · 观异 · 破执:中国人的创新审视三问
人工智能·程序人生
Henry-SAP29 分钟前
SAP MRP类型与计划策略配置精髓解析
人工智能·云原生·sap·erp
未若君雅裁29 分钟前
Agent 的结构化输出,ProviderStrategy、ToolStrategy 与错误重试
python·langchain
筑梦geo32 分钟前
天津口碑好的天津GEO获客公司哪家好 - 成本透明度
人工智能·天津口碑好的
DeepIntelli33 分钟前
GEO 团队选型
人工智能·chatgpt
洛兮银儿33 分钟前
pycharm选中同一个词的标记颜色的修改
ide·python·pycharm