滚动MAD与孤立森林在时序异常检测中的协同应用

在生态监测、工业物联网和设备运维系统中,异常数据往往具有不同表现形式。部分异常十分直观,例如温度瞬间跳变、通信延迟突然增加、传感器读数连续归零。还有一些异常隐藏得更深,单个指标仍处于合理范围,多个指标组合后却偏离了设备长期形成的运行规律。

面对这些情况,工程系统通常会结合统计检测与机器学习检测。**统计方法计算开销较小,适合快速识别短期波动;机器学习方法能够同时分析多个特征,适合发现复杂的异常组合。**滚动 MAD 与孤立森林分别代表这两类思路,前者观察当前数据相对于近期历史的变化,后者判断当前样本在整体数据分布中的孤立程度。


一、时序异常检测的基本思路

时序数据具有连续到达、分布变化和指标相关等特点,固定阈值很难适应全部运行环境。以温度为例,将超过 35℃的数据直接判定为异常,在夏季可能产生大量误报,在冬季又可能错过尚未达到阈值的明显升温。更合理的方式是根据近期历史数据建立动态基线,再判断当前数据是否偏离正常范围。

滚动均值、滚动标准差和滚动 MAD 都可以建立动态基线 。其中,滚动 MAD 对极端值更加稳定,适合处理容易混入异常点的传感器数据。当系统同时采集温度、湿度、电压、风速和通信延迟时,仅对每个指标分别检测仍然不够。某条数据可能在每个维度上都没有越界,它们共同出现时却十分少见,此时就需要引入孤立森林等多维异常检测模型。


二、滚动MAD建立局部基线

1. 滚动窗口与中位数

滚动 MAD 可以理解为一个不断向前移动的观察窗口。假设窗口长度为 20,当第 21 条数据到达时,系统使用前 20 条数据计算中位数和 MAD,再判断当前数据与局部基线之间的距离。下一条数据到达后,窗口向前移动一位,基线也会随近期数据同步更新。

中位数用于描述窗口内数据的中心位置,它不容易被极端值拉动。对于本身就需要识别异常值的任务,这种稳定性十分重要。

2. MAD分数的计算

MAD 表示窗口内各个数据与中位数之间绝对距离的中位数。

bash 复制代码
MAD = median(|xi - median(x)|)

得到 MAD 后,可以进一步计算当前数据的异常分数。

bash 复制代码
MAD_score = 0.6745 × (x - median) / (MAD + ε)

其中,ε 是一个很小的数,用于避免 MAD 等于零时出现除零问题。当分数绝对值超过设定阈值时,当前数据就可以被标记为异常。3.5 是较常见的初始阈值,真实项目仍需结合采样频率、传感器精度和误报情况进行调整

3. 滚动MAD的适用范围

滚动 MAD 特别适合识别突增、突降和短时间偏移。它的计算过程清晰,系统能够直接给出当前值、窗口中位数和 MAD 分数,便于定位异常来源。它主要描述单个指标在时间方向上的变化,对于多个指标之间隐藏的联合关系,仍需要其他方法补充。


三、孤立森林识别多维异常

1. 随机选择特征与分割点

孤立森林是一种无监督异常检测算法模型训练时不需要提前标注正常样本和异常样本,它会不断随机切分数据,并观察每个样本需要经历多少次切分才能被单独分离。模型由多棵孤立树组成,每棵树都会独立执行这一过程。

假设一条监测数据包含温度、湿度和通信延迟三个特征。某棵树开始构建时,会随机选择一个特征,例如温度。当前节点中的温度范围为 20℃到 40℃,模型会在这个范围内随机选择一个分割点,例如 32℃,再将数据划分为两个子集【孤立树-随机切分数据】

bash 复制代码
temperature < 32
temperature >= 32

下一次切分可能继续选择温度,也可能改为湿度或通信延迟。每次选择的特征和分割点都带有随机性,数据会在不断切分中逐渐进入更小的子集,最终落到不同叶节点中。


2. 路径长度与异常程度

假设某个节点中的温度数据如下。

bash 复制代码
27.1  27.3  27.5  27.8  28.0  46.0

如果随机分割点为 35℃,46℃会直接进入一个单独分支,其余数据仍然聚集在另一个分支中。46℃只经历一次切分就被隔离,因此路径长度很短。正常数据彼此接近,需要更多次切分才能逐个分开,路径长度通常更长。

单棵树的判断存在偶然性,某个正常点也可能恰好被较早分离。孤立森林会建立多棵树,再计算同一个样本在不同树中的平均路径长度如果某个数据点在大量随机切分中都能较快被隔离,它通常具有更高的异常概率。


四、两种方法处理不同类型的异常

滚动 MAD 关注当前数据与近期历史之间的差异,孤立森林关注当前样本在多维数据空间中的稀有程度。两者观察数据的角度不同,适合处理的异常类型也有所区别。

例如某设备的温度长期处于 25℃到 32℃之间,湿度长期处于 70%到 90%之间。某一时刻温度为 30℃,湿度为 88%,两个数值单独看都没有超出正常范围,滚动 MAD 可能不会触发异常。如果历史数据中高温通常伴随较低湿度,那么这一组合就可能十分少见。孤立森林同时分析温度和湿度后,可以发现该样本偏离了主要数据群体


五、滚动MAD与孤立森林的协同方式

在实时监测系统中,可以让滚动 MAD 和孤立森林并行处理同一条数据滚动 MAD 输出局部异常结果,孤立森林输出多维异常结果,系统再根据两个模块的判断进行分级

滚动MAD 孤立森林 检测结果
正常 正常 正常
异常 正常 局部异常
正常 异常 组合异常
异常 异常 高置信度异常

只有滚动 MAD 触发时,数据可能发生了短期突变,但整体组合仍接近历史分布。只有孤立森林触发时,单个指标没有明显偏离,多个特征组合后形成了稀有状态。两个模块同时触发时,可以提高告警等级,并优先进入人工复核或自动拦截流程。

先使用滚动 MAD 筛选数据,再将候选样本交给孤立森林复核,也是一种可行方案。这种串行方式能够减少模型调用次数,但容易遗漏单变量正常、多变量组合异常的数据。对于服务端运行的监测系统,并行检测通常能够保留更完整的异常信息。


六、孤立森林需要补充时间特征

孤立森林本身不会自动理解数据的时间顺序。直接输入当前温度、湿度和通信延迟时,模型只能观察当前状态,无法知道温度是否突然升高,也无法判断传感器是否长时间保持同一个数值。

在时序项目中,通常需要主动构造当前值、相邻差值、窗口均值偏差、窗口标准差、连续重复次数和数据到达延迟等特征。假设当前温度为 31℃,这个数值本身可能处于正常范围,但它相对上一分钟升高了 8℃。

bash 复制代码
temperature = 31
temperature_diff = 8

加入差值特征后,孤立森林可以同时观察当前状态和变化速度。原始值描述当前水平,差值描述瞬时变化,窗口偏差描述局部漂移,窗口标准差描述近期波动。经过这些特征补充后,模型会更适合处理连续到达的监测数据。


七、简化的Python实现

下面的代码使用滚动 MAD 检测单变量突变,并使用孤立森林分析多维数据和变化特征。代码构造了温度、湿度和通信延迟三类数据,同时注入温度突增、组合异常和延迟异常。

python 复制代码
import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest


def rolling_mad_score(series, window=20, eps=1e-9):
    """
    使用当前数据之前的历史窗口计算滚动 MAD 分数。
    """
    history = series.shift(1)

    median = history.rolling(
        window=window,
        min_periods=window
    ).median()

    mad = history.rolling(
        window=window,
        min_periods=window
    ).apply(
        lambda values: np.median(
            np.abs(values - np.median(values))
        ),
        raw=True
    )

    return 0.6745 * (series - median) / (mad + eps)


rng = np.random.default_rng(42)
sample_count = 500

data = pd.DataFrame({
    "temperature": rng.normal(28, 1, sample_count),
    "humidity": rng.normal(80, 3, sample_count),
    "latency": rng.normal(0.2, 0.03, sample_count)
})

# 注入三类异常
data.loc[350, "temperature"] = 40
data.loc[420, ["temperature", "humidity"]] = [31, 90]
data.loc[460, "latency"] = 2.5

sensor_columns = [
    "temperature",
    "humidity",
    "latency"
]

# 计算滚动 MAD
for column in sensor_columns:
    data[f"{column}_mad"] = rolling_mad_score(
        data[column]
    )

mad_columns = [
    f"{column}_mad"
    for column in sensor_columns
]

data["mad_flag"] = (
    data[mad_columns]
    .abs()
    .max(axis=1)
    > 3.5
)

# 构造相邻差值特征
for column in sensor_columns:
    data[f"{column}_diff"] = data[column].diff()

feature_columns = [
    "temperature",
    "humidity",
    "latency",
    "temperature_diff",
    "humidity_diff",
    "latency_diff"
]

valid_data = data[feature_columns].dropna()

# 使用前 300 条有效数据训练孤立森林
model = IsolationForest(
    n_estimators=200,
    contamination=0.02,
    random_state=42
)

model.fit(valid_data.iloc[:300])

# 保存孤立森林检测结果
data["if_flag"] = False

data.loc[valid_data.index, "if_flag"] = (
    model.predict(valid_data) == -1
)

# 融合两个模块的结果
data["result"] = "正常"

data.loc[
    data["mad_flag"] & ~data["if_flag"],
    "result"
] = "局部异常"

data.loc[
    ~data["mad_flag"] & data["if_flag"],
    "result"
] = "组合异常"

data.loc[
    data["mad_flag"] & data["if_flag"],
    "result"
] = "高置信度异常"

print(
    data.loc[
        [350, 420, 460],
        [
            "temperature",
            "humidity",
            "latency",
            "mad_flag",
            "if_flag",
            "result"
        ]
    ]
)

这段实现分别保留了滚动 MAD 和孤立森林的检测结果。前者提供局部偏差证据,后者提供多维分布证据。实际系统还可以在两个模块之前加入物理边界规则,直接处理湿度超过 100%、时间戳倒退、设备编号缺失等明确错误,从而形成规则检测、统计检测和机器学习检测相互补充的异常识别结构。

相关推荐
程序员cxuan1 小时前
白嫖 Claude Max 20x 漏洞完整事件始末
人工智能·后端·程序员
颜进强1 小时前
LangChain 从入门到实践:用最小案例理解 RAG 的 5 个核心抽象
前端·后端·ai编程
颜进强1 小时前
MCP 从入门到实践:用 TypeScript 实现第一个 MCP Server
前端·后端·ai编程
Undoom1 小时前
用搜索引擎 API 搭一个 SEO 关键词监控工具:定时追踪排名、广告和相关搜索
后端
程序大爆炸1 小时前
gcsfuse的TypeCache
后端
心运软件1 小时前
Python实战:中国大学排行榜数据采集与可视化大屏
后端·python
GISer_Jing2 小时前
前端转全栈须知后端知识
前端·后端·ai·前端框架
爱勇宝2 小时前
我做了一个排版器,也踩了一遍富文本复制的坑
前端·后端·微信
布朗克1682 小时前
Go 入门到精通-33-unsafe 与 CGO
开发语言·后端·golang·unsafe·cgo