halo 的技术博客

返回

先说结论:别人问「什么是正常」,孤立森林问「谁容易被切出来」——异常点因为少而偏,随机切几刀就被单独隔离,路径短就是异常。 本文合成实验:1000 个交易日注入 25 个崩盘日,孤立森林 AUC 0.984,只看收益幅度的 z-score 基线只有 0.782。差距的来源不是玄学,是多维联动:有些日子单看每一维都不极端,但「放量 + 相关性尖峰 + 波动抬升」同时出现——这种组合异常,单变量方法结构性失明。

孤立森林异常检测:用随机孤立路径给极端行情打分

一、问题:极端行情的「极端」,往往不在单一维度上#

风控里最朴素的异常检测是给收益率算 z-score,超过 3 就报警。它能抓住单日暴跌,但市场真正危险的日子经常长这样:指数只跌了 1.5%(z 大约 1.5,不触发),但成交量放到 3 倍、板块间相关性从 0.3 跳到 0.8——流动性挤兑的典型前兆,每一维单独看都够不上警报线。

多维异常检测的传统路线是「先建正常模型」:估计协方差算马氏距离,或者做密度估计看谁在低密度区。两条路都有硬伤——协方差估计本身会被异常点污染(这正是要检测的东西),密度估计在维度升高后样本需求爆炸。

2008 年 Liu、Ting、Zhou 提出的孤立森林(Isolation Forest)换了个思路:不建模正常,直接建模「容易被孤立」这件事本身。

二、原理:异常点的隔离路径天然更短#

想象在特征空间里随机切刀:每次随机选一个特征、在其取值范围内随机选一个分割点,把数据切成两半,递归下去直到每个点被单独隔开。

关键洞察:异常点少而偏,位于数据云边缘,往往一两刀就被单独切出来;正常点挤在稠密区,要切很多刀才能和邻居分开。 于是「平均隔离路径长度」天然就是异常分数——不需要定义距离,不需要假设分布。

单棵树是随机的、很弱;但对几百棵树的路径长度取平均,噪声互相抵消,信号留下来。分数归一化用:

s(x)=2E[h(x)]c(n)s(x) = 2^{-\frac{E[h(x)]}{c(n)}}

其中 E[h(x)]E[h(x)] 是平均路径长度,c(n)c(n) 是 n 个样本的二叉搜索树平均失败查找深度(归一化常数)。s 接近 1 → 异常;s 在 0.5 以下 → 正常。

三、纯 numpy 实现#

三个部件:建树(随机特征 + 随机分割点)、路径长度(叶子里剩多个点时用 c(n) 补偿)、森林打分。

两个原论文的设计细节容易被忽略:子采样 256 不是省算力的妥协,而是刻意为之——样本太多时正常点会「淹没」异常点周围的空间,小子样反而让异常更容易被早早隔离(swamping/masking 问题);树深上限 log2(256)=8 是因为超过平均深度的路径对区分异常没有增量信息,砍掉省时间。

四、实验:AUC 0.984 vs 单变量基线 0.782#

合成 1000 个交易日,正常日收益 ~N(0.04%, 1%)。注入 25 个崩盘日:收益波动放大 4.5 倍、成交量 z 均值 3.5、相关性尖峰 z 均值 3.0。特征取 4 维:绝对收益、5 日已实现波动、成交量 z、相关性尖峰 z。

方法AUCprecision@25recall@25
孤立森林(4 维)0.98460.0%60.0%
收益 z-score(单变量)0.78244.0%44.0%

异常日的平均分数 0.654,正常日 0.423,分布清晰分离:

分数分布:AUC 0.984,与单变量基线拉开 0.2

在(成交量 z, 相关性 z)平面上看分数热图更直观——孤立森林没有用任何距离度量,却把边缘区域的分数自然抬高了。红圈是真实异常日,绝大多数落在高分区:

特征空间分数热图:隔离不需要距离度量

为什么单变量基线差这么多? 25 个异常日里有相当一部分收益幅度并不惊人(注入的是均值 0 的宽分布,部分样本碰巧接近 0),它们的「异常性」全在量和相关性维度上。z-score 基线对这些日子分数平平;孤立森林在 4 维空间里看到「这个组合位置很偏」,一刀就切出来了。

森林规模的收敛速度快得出乎意料:

树数5102550100200400
AUC0.9640.9730.9840.9860.9850.9870.986

森林规模敏感性:50 棵树后平台化

5 棵树就到 0.964,50 棵基本平台化。这是路径长度平均的方差衰减在起作用——生产上 100~200 棵完全够,不必堆到上千。

五、五个常见陷阱#

陷阱一:「分数超过 0.6 就是异常」——没有绝对阈值。 归一化分数的分布随数据维度、子样本大小、特征相关性变化。同一份代码换个数据集,正常日的分数中枢可能整体平移。正确用法是看分位数(比如 top 2%)或在有标注的历史窗口上校准阈值,绝不硬编码 0.6。

陷阱二:「无监督 = 不用验证」。 无监督只是说训练不用标签,不是说效果不用检验。至少要做:注入已知异常测召回(本文的做法)、人工复核 top-k、与简单基线对比。跳过验证直接上线的异常检测器,报出来的警报没人敢信,很快就会被运维静音——然后在真出事那天一起被静音。

陷阱三:「特征越多越好」。 孤立森林每次分裂随机选特征。如果 20 个特征里只有 3 个携带异常信号,大部分分裂刀切在无关维度上,信号被稀释。特征进模型前先问:这个维度上的极端值,业务上算异常吗?答案模糊的先别放。

陷阱四:「能检测 = 能预测」。 本文检测的是「当天已经是异常日」——事中识别,不是事前预警。把异常分数当预测信号用(今天分数高→明天减仓)需要单独检验分数对未来收益/波动的领先性,那是另一个实验,且大概率残酷得多。事中识别的价值在风控响应速度和数据清洗,别偷换成择时能力。

陷阱五:「训练集必须干净」。 恰恰相反,孤立森林的设计前提就是训练数据里混着异常(它靠异常「少而偏」的性质工作,不靠标签)。真正要担心的是反面情况:异常比例太高(>10%)时,「少」这个前提崩塌,异常点开始互相掩护(masking),路径长度优势消失。这时要么先粗筛降低异常浓度,要么换有监督方法。

六、工程要点#

  • 子采样 256 是特性不是缺陷:加大子样本反而可能因 swamping 效应降低检测力,改之前先做对照实验。
  • 滚动窗口重训:市场的「正常」在漂移,2020 年的正常波动放到 2017 年就是异常。用滚动 1~2 年窗口重建森林,别用全历史。
  • 特征要预先思考方向性:孤立森林对「值域边缘」敏感,对「分布内部的结构异常」不敏感。相关性突变这类信号要先构造成显式特征(本文的 corr z),指望模型从原始价格里自己领悟是幻想。
  • 分数落库、警报分级:连续分数比二值警报有用得多——0.72 和 0.95 的响应等级应该不同。

孤立森林的本质是把「异常 = 少而偏」这个先验直接编译成算法结构。它换来了免距离、免分布假设、近线性复杂度;代价是对「多而不偏」或「偏而不少」的异常失效。知道它的边界在哪,它就是风控工具箱里性价比最高的一把刀。

孤立森林异常检测:用随机孤立路径给极端行情打分
https://blog.halo26812.eu.org/blog/isolation-forest-anomaly
Author halo
Published at 2026年7月24日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨