AutoEncoder时间序列异常检测

一个真实感比较强的模拟案例:

设备每秒采集:入口压力、流量、振动。

训练阶段只有正常数据。

系统每收到一个新数据点,就取最近 60 秒数据,交给 AutoEncoder 重构。

如果重构误差超过阈值,就报警。

最终做到:

传感器数据→60秒滑动窗口→标准化→AutoEncoder→重构→计算误差→阈值判断→异常报警 传感器数据 \rightarrow 60秒滑动窗口 \rightarrow 标准化 \rightarrow AutoEncoder \rightarrow 重构 \rightarrow 计算误差 \rightarrow 阈值判断 \rightarrow 异常报警 传感器数据60秒滑动窗口标准化AutoEncoder重构计算误差阈值判断异常报警


一、先把最终系统想清楚

你最终上线的系统实际上长这样:

压力传感器 ─┐
流量传感器 ─┼──> 实时数据流
振动传感器 ─┘
                 │
                 ▼
          最近60秒数据窗口
                 │
                 ▼
            数据标准化
                 │
                 ▼
          AutoEncoder模型
                 │
           ┌─────┴─────┐
           │           │
        原始数据      重构数据
           │           │
           └─────┬─────┘
                 ▼
             重构误差
                 │
        ┌────────┴────────┐
        │                 │
   Error < 阈值       Error > 阈值
        │                 │
       正常               异常
                          │
                          ▼
                    产生报警事件

初学者首先牢记:

模型并不知道“轴承故障”“阀门卡滞”是什么。

它只知道:

这个数据看起来不像我学过的正常状态。

因此第一阶段我们做的是:

异常检测 \boxed{异常检测} 异常检测

不是:

故障诊断 \boxed{故障诊断} 故障诊断

后面才能继续做:

异常检测→异常定位→故障诊断 异常检测 \rightarrow 异常定位 \rightarrow 故障诊断 异常检测异常定位故障诊断


二、第一步:搭建 Python 环境

建议新建一个目录:

time_series_anomaly/

安装:

pip install numpy pandas matplotlib scikit-learn torch joblib fastapi uvicorn

工程最终可以变成:

time_series_anomaly/
│
├── train.py
├── realtime_detector.py
├── api.py
│
├── ae_model.pt
├── scaler.pkl
└── config.json

现在先不用拆文件。

先写一个:

train.py

把完整训练流程跑起来。


三、第二步:模拟“正常设备数据”

假设设备有三个测点:

pressure    压力 MPa
flow        流量 m³/h
vibration   振动

正常设备不是一条完全直线,而是会正常波动。

import numpy as np
import pandas as pd

np.random.seed(42)

N = 12000

t = np.arange(N)

pressure = (
    2.0
    + 0.08 * np.sin(2 * np.pi * t / 120)
    + np.random.normal(0, 0.02, N)
)

flow = (
    100
    + 4 * np.sin(2 * np.pi * t / 120 + 0.4)
    + np.random.normal(0, 0.8, N)
)

vibration = (
    0.20
    + 0.015 * np.sin(2 * np.pi * t / 40)
    + np.random.normal(0, 0.005, N)
)

df = pd.DataFrame({
    "pressure": pressure,
    "flow": flow,
    "vibration": vibration
})

print(df.head())

你会得到类似:

   pressure      flow   vibration
0   2.0099     101.3      0.204
1   2.0014     101.9      0.202
2   2.0213     101.5      0.206
...

这里的关键点是:

这 12000 个数据全部都是“正常数据”。

它们就是模型的教材。


四、第三步:理解为什么必须标准化

现在三个变量数量级完全不一样:

压力:

2 MPa

流量:

100 m³/h

振动:

0.2

如果直接计算 MSE:

MSE=(X−X^)2 MSE=(X-\hat X)^2 MSE=(XX^)2

那么流量的误差天然比振动大很多。

模型就会产生一种错觉:

流量最重要。

实际上只是单位不同。

所以必须标准化:

x′=x−μσ x'=\frac{x-\mu}{\sigma} x=σxμ

代码:

from sklearn.preprocessing import StandardScaler

FEATURES = [
    "pressure",
    "flow",
    "vibration"
]

split = int(len(df) * 0.7)

train_df = df.iloc[:split]
val_df = df.iloc[split:]

scaler = StandardScaler()

train_scaled = scaler.fit_transform(
    train_df[FEATURES]
)

val_scaled = scaler.transform(
    val_df[FEATURES]
)

有一个工程细节非常重要:

fit()只能在训练数据上做。

不能这样:

scaler.fit_transform(全部数据)

否则就是:

数据泄漏 \boxed{数据泄漏} 数据泄漏


五、第四步:为什么不能每次只输入一个数据点

如果某时刻:

压力 = 2.10 MPa

单独看它可能没有问题。

但假如最近的数据是:

2.00
2.02
2.06
2.10
2.17
2.25
2.38

那么问题不是:

2.10 是否异常。

而是:

压力正在持续快速上涨。

所以时间序列模型需要看一个窗口。

例如:

Window=60 Window=60 Window=60

也就是一次看最近 60 秒。

t-59
t-58
...
t-2
t-1
t

三个传感器:

60 × 3

因此一个样本实际上有:

60×3=180 60\times3=180 60×3=180

个数字。


六、第五步:构造滑动窗口

写:

WINDOW = 60


def make_windows(data, window=60):
    X = []

    for i in range(len(data) - window + 1):

        block = data[i:i + window]

        X.append(
            block.reshape(-1)
        )

    return np.array(X)

然后:

X_train = make_windows(
    train_scaled,
    WINDOW
)

X_val = make_windows(
    val_scaled,
    WINDOW
)

print(X_train.shape)

可能输出:

(8341, 180)

也就是说:

8341 个训练样本

每个样本:

180维

七、现在真正理解一下一个训练样本是什么

模型看到的不是:

压力 = 2.01

而是:

最近60秒:

pressure:
2.01
2.02
2.03
2.01
...

flow:
101
102
101
100
...

vibration:
0.201
0.203
0.199
...

最终展开成:

[
 p1, f1, v1,
 p2, f2, v2,
 p3, f3, v3,
 ...
 p60, f60, v60
]

总共:

180个数字

八、第六步:建立最简单的 AutoEncoder

先不要用 Transformer。

先做一个最容易理解、最容易上线的全连接 AE。

import torch
import torch.nn as nn


class AutoEncoder(nn.Module):

    def __init__(self, input_dim):

        super().__init__()

        self.encoder = nn.Sequential(

            nn.Linear(input_dim, 128),
            nn.ReLU(),

            nn.Linear(128, 32),
            nn.ReLU(),

            nn.Linear(32, 8)
        )

        self.decoder = nn.Sequential(

            nn.Linear(8, 32),
            nn.ReLU(),

            nn.Linear(32, 128),
            nn.ReLU(),

            nn.Linear(128, input_dim)
        )

    def forward(self, x):

        z = self.encoder(x)

        x_hat = self.decoder(z)

        return x_hat

这个网络本质是:

180
 ↓
128
 ↓
32
 ↓
8
 ↑
32
 ↑
128
 ↑
180

最关键的是:

180 → 8

模型被迫把:

60秒 × 3传感器

压缩成:

8个数字

九、为什么要故意压缩?

这是 AutoEncoder 的灵魂。

如果:

180 → 1000 → 1000 → 180

模型能力太强,有可能直接:

把输入复制到输出。

这就失去意义了。

我们故意让它:

180 → 8

相当于告诉模型:

不能死记硬背,你必须找到正常设备真正的核心规律。

比如模型可能隐式学会:

压力大概怎么变化

流量跟压力什么关系

振动应该多大

60秒内趋势怎样变化

十、第七步:训练模型

先转换成 PyTorch Tensor:

X_train_tensor = torch.tensor(
    X_train,
    dtype=torch.float32
)

X_val_tensor = torch.tensor(
    X_val,
    dtype=torch.float32
)

创建模型:

input_dim = X_train.shape[1]

model = AutoEncoder(input_dim)

损失函数:

criterion = nn.MSELoss()

优化器:

optimizer = torch.optim.Adam(
    model.parameters(),
    lr=0.001
)

训练:

EPOCHS = 30
BATCH_SIZE = 128


for epoch in range(EPOCHS):

    model.train()

    permutation = torch.randperm(
        X_train_tensor.size(0)
    )

    total_loss = 0

    for i in range(
        0,
        X_train_tensor.size(0),
        BATCH_SIZE
    ):

        indices = permutation[
            i:i+BATCH_SIZE
        ]

        batch = X_train_tensor[indices]

        optimizer.zero_grad()

        reconstructed = model(batch)

        loss = criterion(
            reconstructed,
            batch
        )

        loss.backward()

        optimizer.step()

        total_loss += loss.item()

    print(
        f"Epoch {epoch+1:02d}, "
        f"Loss={total_loss:.4f}"
    )

这时候发生了什么?

模型每次拿到:

X X X

然后生成:

X^ \hat X X^

训练目标就是:

X≈X^ X\approx\hat X XX^

损失:

L=1N∑(X−X^)2 L= \frac1N \sum (X-\hat X)^2 L=N1(XX^)2

不断让:

L↓ L\downarrow L


十一、训练完成以后,模型学到了什么?

非常重要。

它并没有学:

故障A
故障B
故障C

它学的是:

正常运行空间 \boxed{\text{正常运行空间}} 正常运行空间

可以把它想象成:

                    异常 ×


        ● ● ● ● ●
      ● ● ● ● ● ● ●
       ● ● 正常 ● ●
      ● ● ● ● ● ●
        ● ● ● ●

                         × 异常

              × 异常

这些:

是正常样本。

AutoEncoder学到了这个正常区域。


十二、第八步:计算正常数据的重构误差

训练完成后:

model.eval()

with torch.no_grad():

    reconstructed = model(
        X_val_tensor
    )

    errors = torch.mean(
        (X_val_tensor - reconstructed) ** 2,
        dim=1
    )

errors = errors.numpy()

现在:

errors

可能长这样:

0.021
0.019
0.025
0.031
0.018
0.028
...

这些都是:

正常样本重构误差 正常样本重构误差 正常样本重构误差


十三、第九步:确定报警阈值

这是整个异常检测系统里非常关键的一步。

例如取:

threshold = np.quantile(
    errors,
    0.995
)

print(
    "threshold =",
    threshold
)

意思是:

正常数据 99.5% 的重构误差都小于这个值。

假设:

threshold = 0.082

那么:

Error <= 0.082

认为正常。

而:

Error > 0.082

认为异常。

即:

Error>τ⇒异常 \boxed{ Error>\tau \Rightarrow 异常 } Error>τ异常


十四、第十步:人为制造几个设备异常

现在进入最有意思的阶段。

我们制造一段测试数据。

N_TEST = 3000

t2 = np.arange(
    N,
    N + N_TEST
)

pressure_test = (
    2.0
    + 0.08 * np.sin(
        2*np.pi*t2/120
    )
    + np.random.normal(
        0,
        0.02,
        N_TEST
    )
)

flow_test = (
    100
    + 4*np.sin(
        2*np.pi*t2/120 + 0.4
    )
    + np.random.normal(
        0,
        0.8,
        N_TEST
    )
)

vibration_test = (
    0.20
    + 0.015*np.sin(
        2*np.pi*t2/40
    )
    + np.random.normal(
        0,
        0.005,
        N_TEST
    )
)

现在人为制造三个异常。


异常1:压力突然升高

pressure_test[
    700:760
] += 0.6

相当于:

正常:

2.01
2.03
2.04

突然:

2.58
2.61
2.65

十五、异常2:振动突然变大

vibration_test[
    1400:1500
] += 0.15

正常:

0.20

异常:

0.35

十六、异常3:压力慢慢漂移

这个更加符合工业现场。

pressure_test[
    2100:2300
] += np.linspace(
    0,
    0.5,
    200
)

开始:

2.00

逐渐:

2.05
2.10
2.20
2.30
2.40
2.50

这种异常传统固定阈值很可能发现得晚。

而时序模型可能提前发现:

趋势已经不像正常运行了。


十七、组合测试数据

test_df = pd.DataFrame({
    "pressure": pressure_test,
    "flow": flow_test,
    "vibration": vibration_test
})

注意:

这里不能重新:

fit_transform

必须:

test_scaled = scaler.transform(
    test_df[FEATURES]
)

原因:

上线以后必须保持训练阶段的数据尺度。


十八、生成测试窗口

X_test = make_windows(
    test_scaled,
    WINDOW
)

X_test_tensor = torch.tensor(
    X_test,
    dtype=torch.float32
)

十九、计算测试异常分数

model.eval()

with torch.no_grad():

    reconstructed_test = model(
        X_test_tensor
    )

    test_errors = torch.mean(
        (
            X_test_tensor
            - reconstructed_test
        ) ** 2,
        dim=1
    ).numpy()

现在:

test_errors

就是每个时间窗口的:

异常分数 \boxed{异常分数} 异常分数


二十、真正产生报警

anomaly_flags = (
    test_errors > threshold
)

例如:

时间     Error    状态

650     0.028    正常
651     0.031    正常
652     0.025    正常

705     0.16     异常
706     0.28     异常
707     0.41     异常

...

这就是第一版工业异常检测。


二十一、把异常分数画出来

import matplotlib.pyplot as plt

plt.figure(figsize=(14, 5))

plt.plot(
    test_errors,
    label="Reconstruction Error"
)

plt.axhline(
    threshold,
    linestyle="--",
    label="Threshold"
)

plt.xlabel("Time Window")
plt.ylabel("Reconstruction Error")

plt.legend()

plt.show()

你应该看到:

Error

  │
1 │                    /\               /\
  │                   /  \             /  \
  │
τ │------阈值--------------------------------
  │
  │____正常_______/________正常___/__________
  └────────────────────────────────── 时间

这时候初学者通常就真正理解了:

系统根本没有训练“压力异常”。

但压力异常出现以后:

X X X

和:

X^ \hat X X^

差异明显变大。

于是:

Error↑ Error\uparrow Error


二十二、进一步看“模型到底哪里重构错了”

这一步对于理解特别重要。

取一个异常窗口:

index = 720

original = X_test[index]

with torch.no_grad():

    reconstructed = model(
        torch.tensor(
            original,
            dtype=torch.float32
        ).unsqueeze(0)
    ).numpy()[0]

恢复为:

60 × 3
original = original.reshape(
    WINDOW,
    3
)

reconstructed = reconstructed.reshape(
    WINDOW,
    3
)

例如画压力:

plt.figure(figsize=(12, 4))

plt.plot(
    original[:, 0],
    label="Real Pressure"
)

plt.plot(
    reconstructed[:, 0],
    label="Reconstructed Pressure"
)

plt.legend()

plt.show()

正常情况下你会看到:

真实       ~~~~~~~
重构       ~~~~~~~

几乎重合。

异常情况下:

真实            /\
               /  \
              /    \

重构      ~~~~~~~~~~~~~

模型相当于在说:

“按照我的正常经验,这里本来不应该突然冲这么高。”


二十三、到这里你已经完成了离线异常检测

现在完整流程已经是:

历史正常数据
      ↓
数据清洗
      ↓
标准化
      ↓
滑动窗口
      ↓
AutoEncoder训练
      ↓
正常误差分布
      ↓
确定阈值
      ↓
保存模型

测试:

新数据
 ↓
滑动窗口
 ↓
标准化
 ↓
AutoEncoder
 ↓
重构误差
 ↓
阈值比较
 ↓
正常 / 异常

下面开始真正工程化。


二十四、第十一步:保存模型

训练完成:

import torch
import joblib
import json


torch.save(
    {
        "model_state_dict":
            model.state_dict(),

        "input_dim":
            input_dim,

        "window":
            WINDOW,

        "features":
            FEATURES
    },
    "ae_model.pt"
)

保存标准化模型:

joblib.dump(
    scaler,
    "scaler.pkl"
)

保存阈值:

with open(
    "config.json",
    "w",
    encoding="utf-8"
) as f:

    json.dump(
        {
            "threshold":
                float(threshold),

            "window":
                WINDOW
        },
        f,
        indent=2
    )

现在你的工程中有:

ae_model.pt
scaler.pkl
config.json

二十五、第十二步:做真正的实时检测

现场不是:

一次给你 3000 条数据。

而是:

10:01:01 来1条
10:01:02 来1条
10:01:03 来1条
...

所以需要一个缓冲区:

最近60条数据

Python可以用:

deque

二十六、实时检测器

创建:

realtime_detector.py

代码:

from collections import deque

import json
import joblib
import numpy as np
import torch
import torch.nn as nn


class AutoEncoder(nn.Module):

    def __init__(self, input_dim):

        super().__init__()

        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 32),
            nn.ReLU(),
            nn.Linear(32, 8)
        )

        self.decoder = nn.Sequential(
            nn.Linear(8, 32),
            nn.ReLU(),
            nn.Linear(32, 128),
            nn.ReLU(),
            nn.Linear(128, input_dim)
        )

    def forward(self, x):

        z = self.encoder(x)

        return self.decoder(z)


class RealtimeDetector:

    def __init__(self):

        checkpoint = torch.load(
            "ae_model.pt",
            map_location="cpu"
        )

        self.window = checkpoint["window"]

        self.features = checkpoint["features"]

        self.scaler = joblib.load(
            "scaler.pkl"
        )

        with open(
            "config.json",
            "r",
            encoding="utf-8"
        ) as f:

            config = json.load(f)

        self.threshold = config["threshold"]

        self.model = AutoEncoder(
            checkpoint["input_dim"]
        )

        self.model.load_state_dict(
            checkpoint["model_state_dict"]
        )

        self.model.eval()

        self.buffer = deque(
            maxlen=self.window
        )

    def add_point(
        self,
        pressure,
        flow,
        vibration
    ):

        row = [
            pressure,
            flow,
            vibration
        ]

        self.buffer.append(row)

        if len(self.buffer) < self.window:

            return {
                "ready": False,
                "message":
                    f"正在积累数据 "
                    f"{len(self.buffer)}/{self.window}"
            }

        data = np.array(
            self.buffer
        )

        data_scaled = self.scaler.transform(
            data
        )

        x = data_scaled.reshape(
            1,
            -1
        )

        x_tensor = torch.tensor(
            x,
            dtype=torch.float32
        )

        with torch.no_grad():

            x_hat = self.model(
                x_tensor
            )

            error = torch.mean(
                (
                    x_tensor
                    - x_hat
                ) ** 2
            ).item()

        is_anomaly = (
            error > self.threshold
        )

        return {
            "ready": True,
            "score": error,
            "threshold": self.threshold,
            "is_anomaly": is_anomaly
        }

二十七、模拟设备实时发送数据

写:

from realtime_detector import RealtimeDetector


detector = RealtimeDetector()


for i, row in test_df.iterrows():

    result = detector.add_point(
        row["pressure"],
        row["flow"],
        row["vibration"]
    )

    if (
        result["ready"]
        and result["is_anomaly"]
    ):

        print(
            "异常报警",
            i,
            result
        )

这样就是:

传感器
↓
实时进入
↓
60秒缓存
↓
AI检测
↓
报警

二十八、但是工业现场绝对不能“Error一超阈值就报警”

这是从学生实验走向工程系统最重要的一步。

因为偶尔一个噪声:

Error > threshold

很正常。

如果直接报警:

滴滴滴
滴滴滴
滴滴滴

现场人员几天之后就把系统关掉了。

所以应该增加:

持续性判断 \boxed{持续性判断} 持续性判断

例如:

最近5次里面至少3次异常,才产生正式报警。

即:

0 0 1 0 0

不报警。

但:

1 0 1 1 1

报警。


二十九、增加“3/5报警机制”

可以增加:

self.score_history = deque(
    maxlen=5
)

每次:

self.score_history.append(
    error > self.threshold
)

然后:

alarm = (
    sum(self.score_history) >= 3
)

这样误报警会明显下降。


三十、更工程化:设置四级异常

不要只做:

正常 / 异常

可以变成:

正常
关注
预警
报警

比如:

S=ErrorThreshold S=\frac{Error}{Threshold} S=ThresholdError

定义:

S < 0.8       正常
0.8~1.0       关注
1.0~2.0       黄色预警
>2.0          红色报警

代码:

ratio = error / self.threshold

if ratio < 0.8:
    level = "NORMAL"

elif ratio < 1.0:
    level = "WATCH"

elif ratio < 2.0:
    level = "WARNING"

else:
    level = "ALARM"

这个就已经非常接近工业系统了。


三十一、下一步:解决“到底哪个变量异常”

这是非常重要的一步。

如果系统只告诉操作员:

AI异常分数 = 0.46

操作员会说:

所以呢?

必须告诉他:

压力异常
还是
流量异常
还是
振动异常

可以计算每个变量自己的重构误差:

Ej=1T∑t(xt,j−x^t,j)2 E_j= \frac1T \sum_t (x_{t,j}-\hat x_{t,j})^2 Ej=T1t(xt,jx^t,j)2

代码:

real = x_tensor.numpy().reshape(
    self.window,
    3
)

recon = x_hat.numpy().reshape(
    self.window,
    3
)

feature_errors = np.mean(
    (real - recon) ** 2,
    axis=0
)

得到:

pressure_error  = 0.52
flow_error      = 0.03
vibration_error = 0.02

那么就可以告诉用户:

主要异常变量:

1. 压力
2. 流量
3. 振动

三十二、系统输出应该从这样

不要:

{
  "score": 0.63
}

而应该输出:

{
  "device_id": "REG-001",
  "status": "WARNING",
  "score": 0.63,
  "threshold": 0.12,

  "main_abnormal_feature": "pressure",

  "feature_errors": {
    "pressure": 0.51,
    "flow": 0.07,
    "vibration": 0.05
  }
}

这才有工程意义。


三十三、第十三步:把模型变成 API

现场的 SCADA、WinForms、Java、C# 系统通常不会直接运行 Python 模型。

最好做成:

HTTPAPI HTTP API HTTPAPI

例如:

POST /detect

创建:

api.py

三十四、FastAPI版本

from fastapi import FastAPI
from pydantic import BaseModel

from realtime_detector import (
    RealtimeDetector
)


app = FastAPI(
    title="Time Series Anomaly API"
)

detector = RealtimeDetector()


class SensorData(BaseModel):

    pressure: float
    flow: float
    vibration: float


@app.post("/detect")

def detect(data: SensorData):

    result = detector.add_point(
        pressure=data.pressure,
        flow=data.flow,
        vibration=data.vibration
    )

    return result

启动:

uvicorn api:app \
    --host 0.0.0.0 \
    --port 8000

浏览器打开:

http://localhost:8000/docs

你会看到自动生成的 API 页面。


三十五、现场系统怎么调用

例如发送:

{
    "pressure": 2.03,
    "flow": 101.2,
    "vibration": 0.203
}

API返回:

{
    "ready": true,
    "score": 0.031,
    "threshold": 0.086,
    "is_anomaly": false
}

异常时:

{
    "ready": true,
    "score": 0.537,
    "threshold": 0.086,
    "is_anomaly": true
}

这样:

PLC
SCADA
WinForms
Web
MES
边缘网关

都可以调用。


三十六、真正上线以后不能这样设计

现在这个:

detector = RealtimeDetector()

只有:

一个buffer

所以只适合:

一台设备

真实现场可能有:

REG-001
REG-002
REG-003
...

每台设备都必须维护自己的:

60秒窗口

结构应该变成:

设备ID
   │
   ├── REG001 → Buffer
   │
   ├── REG002 → Buffer
   │
   └── REG003 → Buffer

代码思想:

detectors = {}


def get_detector(device_id):

    if device_id not in detectors:

        detectors[device_id] = (
            RealtimeDetector()
        )

    return detectors[device_id]

这就变成多设备了。


三十七、真正生产系统建议这样设计

最终架构:

┌─────────────────────┐
│   PLC / RTU / SCADA  │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│      数据采集层       │
│ OPC UA / MQTT / HTTP │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│     实时数据缓存      │
│ Redis / 内存 / Kafka │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│    异常检测服务       │
│ AutoEncoder          │
│ Reconstruction Error│
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│     报警规则引擎      │
│ 阈值 + 持续时间       │
│ 3/5 + 抑制 + 去抖     │
└──────────┬──────────┘
           │
     ┌─────┴─────┐
     ▼           ▼
   SCADA       Web平台
   报警         风险看板

三十八、上线时一定要保存5样东西

很多论文模型不能上线,就是漏掉这些。

必须保存:

1. 模型

ae_model.pt

2. 标准化器

scaler.pkl

3. 阈值

threshold

4. 特征顺序

必须固定:

pressure
flow
vibration

绝对不能训练是:

pressure, flow, vibration

上线变成:

flow, pressure, vibration

模型会直接废掉。

5. 窗口长度

例如:

60

训练和上线必须一致。


三十九、建议再保存一个完整配置文件

例如:

{
    "model_version": "1.0.0",

    "window_size": 60,

    "sample_interval_sec": 1,

    "features": [
        "pressure",
        "flow",
        "vibration"
    ],

    "threshold": 0.086,

    "alarm_rule": {
        "window": 5,
        "min_abnormal_count": 3
    }
}

这非常重要。

因为半年以后你自己都会忘:

当时到底怎么训练的。


四十、真正工业数据还必须做数据质量判断

在送给 AI 之前一定要判断:

有没有空值?
有没有断线?
有没有恒定值?
有没有明显超量程?
有没有时间戳乱序?

例如:

if pressure is None:
    return "DATA_INVALID"

如果传感器压力量程:

0~10MPa

收到:

99999MPa

不要让AI判断。

应该直接:

传感器数据质量异常

所以真正系统是:

数据质量检测→AI异常检测 数据质量检测 \rightarrow AI异常检测 数据质量检测AI异常检测

而不是直接AI。


四十一、工业现场推荐“四层判断”

如果你以后做燃气调压器、泵、压缩机,我建议采用:

第一层:数据质量

第二层:传统安全阈值

第三层:AI异常检测

第四层:故障诊断

比如:

压力 > 安全上限

这种事情千万不要等 AI。

直接报警。

AI主要负责发现:

还没越限,但是运行模式已经不正常。

这才是AI真正有价值的地方。


四十二、举一个现场案例

正常情况:

入口压力       4.0 MPa
出口压力       2.0 MPa
流量           100
振动           0.20

某天:

入口压力       正常
出口压力       2.05
流量           正常
振动           0.23

所有数据都:

没有超过传统阈值

所以SCADA:

不报警

但最近60分钟模式开始变化:

出口压力波动越来越大

振动逐渐增加

压力和流量关系开始改变

AE发现:

Error:0.02→0.03→0.05→0.09→0.16 Error: 0.02 \rightarrow 0.03 \rightarrow 0.05 \rightarrow 0.09 \rightarrow 0.16 Error:0.020.030.050.090.16

如果阈值:

τ=0.08 \tau=0.08 τ=0.08

系统在:

0.09

就开始预警。

这就是:

从“超限报警”变成“模式异常预警”。


四十三、这才是你的系统真正应该显示的界面

建议以后界面不要只显示一个AI分数。

应该有:

设备:调压器 REG-001

状态:黄色预警

当前异常分数:
0.126

正常阈值:
0.086

异常程度:
1.47 × threshold

主要异常变量:

1. 出口压力      58%
2. 振动          27%
3. 流量          15%

最近趋势:
持续升高 ↑

持续时间:
6 min

建议:
关注出口压力波动及调压机构状态

这样现场人员才能真正使用。


四十四、从实验代码到真正上线,我建议分成6级

Level 1

CSV
+
AE
+
重构误差

证明算法可行。


Level 2

真实历史数据
+
滑动窗口
+
正常训练

验证真实设备。


Level 3

增加:

变量级异常贡献

知道哪个传感器异常。


Level 4

增加:

持续时间
3/5策略
去抖
报警抑制

解决现场误报警。


Level 5

接:

OPC UA / MQTT / SCADA

做到实时运行。


Level 6

增加:

故障知识库
+
规则
+
机理
+
AI异常检测

最终变成:

智能故障诊断系统 \boxed{智能故障诊断系统} 智能故障诊断系统


四十五、一定要理解一个非常重要的问题

AutoEncoder最适合做:

“你现在不像正常状态。”

它不是天然擅长:

“你一定是阀杆卡涩。”

例如:

Error = 0.58

只能可靠说明:

异常 \boxed{异常} 异常

然后进一步看:

压力贡献最大
振动次之

再结合规则:

出口压力持续振荡
+
振动升高
+
入口压力稳定

才可能判断:

疑似调压机构振荡/卡涩

所以真正高级的故障诊断系统是:

AI异常检测+变量定位+规则+机理+故障知识 \boxed{ AI异常检测 + 变量定位 + 规则 + 机理 + 故障知识 } AI异常检测+变量定位+规则+机理+故障知识

而不是一个 AE 包打天下。


四十六、给初学者一个最值得记住的完整逻辑

你可以把整个系统想成一个只接受过“健康体检”的医生。

第一步

让医生天天看:

健康人的心电图

第二步

医生逐渐形成:

健康应该长什么样

第三步

来了一个新病人。

医生尝试按照自己的“健康规律”还原。


第四步

如果:

真实 ≈ 健康模型重构

说明:

正常

第五步

如果:

真实 ≠ 重构

说明:

偏离正常

最终就是:

正常数据→学习正常规律→实时数据→重构→误差→异常分数→持续性判断→报警 \boxed{ 正常数据 \rightarrow 学习正常规律 \rightarrow 实时数据 \rightarrow 重构 \rightarrow 误差 \rightarrow 异常分数 \rightarrow 持续性判断 \rightarrow 报警 } 正常数据学习正常规律实时数据重构误差异常分数持续性判断报警

如果以你前面一直研究的燃气调压器、泵、压缩机等工业设备故障诊断为目标,下一步最值得做的,不是马上换 Transformer,而是把这个示例继续升级成一个真正的:

压力+振动+声纹→实时AutoEncoder异常检测→异常变量定位→故障诊断→FastAPI→WinForms/SCADA调用 \boxed{ 压力+振动+声纹 \rightarrow 实时AutoEncoder异常检测 \rightarrow 异常变量定位 \rightarrow 故障诊断 \rightarrow FastAPI \rightarrow WinForms/SCADA调用 } 压力+振动+声纹实时AutoEncoder异常检测异常变量定位故障诊断FastAPIWinForms/SCADA调用

的完整工程。这样你会把“论文里的重构异常检测”和“现场真正能跑的软件”一次彻底打通。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值