1. 为什么我选了LSTM-AE做交易时序异常检测
这周本来继续做特征工程,结果翻到前几周留存的一些极端行情日志,发现手工规则筛异常样本的办法越来越鸡肋。阈值设紧一点,闪崩那几天确实能抓出来,但常规波动稍大就会误报;阈值放松,市场结构异变又经常漏。规则永远在追着行情跑。所以我把本周任务定成:用无监督方式把"正常交易模式"学出来,再拿重构误差当异常分数。选型一圈,最终落在LSTM-AE上。
LSTM-AE做的事情可以这样理解:它先学会一套"正常行情长什么样"的压缩规则,然后给你一段新的行情窗口,它尝试用这套规则把窗口重建出来。如果重建得七七八八,说明这段行情符合历史常态;如果重建出来的东西和原窗口差得离谱,说明这段行情里存在它没见过的结构,这就是异常信号。
我见过有人一上来就上Transformer或扩散模型做异常检测,不是说不行,而是对量化交易场景来说,LSTM-AE有几个非常实用的特质:
- 天然适配变长序列的依赖关系:交易数据是典型的时间序列,当前量价特征往往和过去几十个交易日的状态相关,LSTM的门控机制能把这种长期依赖压进隐状态;
- 无监督训练成本低:不需要人工标注哪些是股灾、哪些是乌龙指、哪些是流动性冲击,直接拿历史正常窗口训练即可;
- 重构误差的含义直观:MSE或者MAE算出来的数字,可以直接当成"异常分数"来排序,风控同事也好理解;
- 部署轻量:相比生成式模型,LSTM-AE的参数量小一个量级,CPU上也能跑推理,实盘链路里加一个检测服务并不重。
从任务定位来说,我把问题形式化成了这样:给定一段长度为T、特征维度为F的多元时间序列窗口 (X \in R^{T \times F}),模型先通过编码器把窗口压缩成一个低维上下文向量 (z),再通过解码器重建出 (\hat{X} \in R^{T \times F}),异常分数定义为重构误差 (e = |X - \hat{X}|)。正常样本对应较小的 (e),异常样本对应较大的 (e)。
这个定义有一个隐含前提:正常样本必须在训练集中占绝对主导。如果训练集里混入了大量异常窗口,模型会把异常也学进去,等于把异常当正常,那检测就失效了。所以训练集的清洗仍然是前置工作,这一点后面细聊。
我们的落地场景是A股和股指期货的日内与日间混合数据,基础特征包括收益、成交量、成交额、振幅、波动率、买卖价差等,再加上一些衍生特征,每个时间步的特征维度固定。我把这个模型定位为"盘后扫描工具",不追求毫秒级告警,主要用来辅助复盘和风控排查。
2. 从LSTM到自编码器:理解这个"正常模式压缩器"
2.1 先拆开看:编码器在做什么
自编码器本身不复杂,就是"压缩再还原",但把它用在时间序列上时,压缩的不再是一张图或一个向量,而是一段有先后顺序的序列。这时候普通全连接层就不好用了,因为全连接层对输入特征的顺序不敏感,把第1天和第60天的数据互换位置,它压缩出来的结果几乎一样——这对交易数据来说是不可接受的。
LSTM的引入恰好解决顺序问题。编码器部分用LSTM逐步读取窗口中的每一个时间步,维护一个隐状态 (h_t),最后把整个窗口的信息浓缩在最后一个时间步的隐状态里,或者把最后一个隐状态再经过一个全连接层映射成固定长度的上下文向量 (z)。
我用一个生活化的类比来解释这个过程:想象一个值班多年的风控员,每天看盘口、看成交、看波动,他脑子里逐渐形成了一套关于"正常市场节奏"的直觉,不需要看完整段行情,只需要瞄一眼关键特征就能说出"今天不对劲"。LSTM编码器就好比这个风控员的眼睛和记忆,它逐个交易日观察数据,把观察到的规律沉淀在隐状态里。最后那个上下文向量 (z),就是他脑子里的那套"市场正常状态图谱"。
2.2 解码器:从脑子里把正常画面复现出来
解码器的任务正好相反:拿到上下文向量 (z),要把它展开成一段和输入长度相同的序列。在LSTM-AE的实现里,最常见的方式是让解码器以 (z) 作为初始隐状态,然后逐时间步生成重建数据;也有的实现会把 (z) 在每个时间步都拼接到解码器的输入上,这样解码器每个时刻都能"看到"压缩后的全局信息,重建稳定性更好。
继续上面的类比:解码器就是风控员根据自己的记忆,去完整复述一遍"正常的一天应该是什么样"。如果他能非常顺畅地把波动率、成交量、价差这些特征按正常节奏复述出来,说明今天没有什么异常;如果复述到某个时段突然卡壳,怎么都想不起来"这个量级的成交配合这个级别的波动"是什么时候见过,那大概率就是出事了。
2.3 重构误差:异常分数怎么算
输入 (X) 和重建结果 (\hat{X}) 之间的误差就是异常分数。常用的计算方式有三种:
- 均方误差(MSE):对每个位置每个特征求差的平方再平均,对大误差更敏感;
- 平均绝对误差(MAE):对每个位置每个特征求绝对差再平均,对极端值的惩罚比MSE温和;
- 分段加权误差:对近期时间步给更高权重,适合突出"最近几天才出现的变化"。
我第一版直接用MSE。后来对比发现,MSE在极端行情下会被个别特征的巨大偏差主导,比如某一天成交量突然放大10倍,重建误差几乎全由成交量贡献,其他维度的异常被淹没了。所以后来我改成了按特征归一化的加权MSE:训练时统计每个特征的重构误差均值和标准差,推理时用标准化后的误差进行加权求和,这样不同量纲的特征才能公平地贡献异常分数。
2.4 训练目标:逼着模型只学正常路径
自编码器的训练目标就是最小化重构误差,这在异常检测里其实是一个反直觉却有效的设计:我们故意不给模型看异常样本,让它只知道正常样本怎么压缩、怎么还原。训练完成之后,模型对正常模式的重建能力很强,而对没见过的异常模式,压缩时会丢失关键信息,重建时自然出现较大偏差。
为保证这一点,训练数据准备上要做几个动作:
- 用滚动窗口切分数据,窗口长度我设为60个交易日,步长5天,降低相邻窗口的重复度;
- 剔除停牌、涨跌停附近、重大事件窗口等明显异常时段,这部分用标签数据辅助过滤;
- 对每个特征做标准化,防止高幅值特征主导误差计算;
- 按时间顺序切分训练集和验证集,避免未来信息泄漏。
3. 真实场景推演:闪崩、流动性枯竭与消息冲击下的表现
很多文章喜欢用理论推导证明LSTM-AE能检测异常,但真实行情里异常模式不是单一类型。我用三类典型事件来推演这个模型的表现。
3.1 闪崩:量价齐升的瞬间爆发
闪崩场景下,市场通常在极短时间内出现剧烈的价格下跌和成交量激增。某只指数权重股可能几分钟内下跌5%,同时成交额放大到平时几倍。
此时LSTM-AE的重建过程会是这样的:编码器读取这段窗口,发现价格序列呈现快速下跌趋势,这与它在训练中学习到的"价格缓慢波动"模式完全不同,压缩到上下文向量时,为了保留整体趋势特征,会牺牲掉部分细节。解码器尝试用正常模式重建,得到的价格曲线会比较平缓,无法还原出陡峭下跌的形状。最终MSE会显著偏大,尤其是在价格和成交量两个维度上。
这类异常通常能非常有效地被捕获,因为正常行情里极少出现"单位时间内价格变化幅度"和"量能配合"同时突破历史分布的现象。
3.2 流动性枯竭:缓慢而隐蔽的异常
流动性枯竭更难检测。市场通常表现为成交量持续萎缩、买卖价差逐步拉大、盘口深度变薄,但这些变化是渐进的,不像闪崩那样在几十秒内爆发。
LSTM-AE对这类异常的灵敏度取决于窗口内特征的演变速度。如果枯竭过程持续两周,相关特征在60日窗口内缓慢移动,模型可能把这些渐变当作正常市场状态的一部分,重构误差并不会很大。这时候需要额外引入更高频的微观结构特征,例如订单簿不平衡指标、有效价差、深度衰减率,才能让模型捕捉到"成交节奏的质变"。
实测中我发现,LSTM-AE对渐变型异常的检测能力有限,这是它的固有短板,不是调参能完全解决的。因此我通常会把它和基于统计的滚动异常检测器并联使用,统计检测器负责渐变偏离,LSTM-AE负责突发结构异变。
3.3 重大消息冲击:高波动与相关性突变
重大新闻冲击往往伴随着波动率跳升、板块间相关性重构、成交量异常放大等特征。这种情况下LSTM-AE的重构误差确实会变大,但有一个需要警惕的现象:如果历史训练集中包含过若干次类似的"消息冲击"事件,模型可能已经把它们压缩进了"正常模式"的范围。
举个例子,某训练集覆盖了多次降准和突发政策公告,这些事件都伴随类似的量价反应,那么在训练后,模型会把"波动率在一两个交易日内上升2倍"当作一种常见模式,重构误差不会特别大。但真实的消息冲击往往叠加了情绪踩踏和流动性冲击,量价反应比历史上任何一次都剧烈,只要反应幅度超出训练分布,重构误差依然会拉开差距。
所以关键不在于"模型能否区分正常与异常",而在于"训练分布覆盖了多宽的异常范围"。覆盖越窄,越容易检测出新异变;覆盖太宽,异常检测就变成了"离群点之外再找离群点",灵敏度反而下降。在实践中,我会刻意在训练集中剔除那些已经归因的异常窗口,只保留相对干净的正常市场状态数据。
4. 为什么你的LSTM-AE对异常样本不敏感?几个常见的坑
社区里经常能看到类似问题:"跑出来的重构误差在正常和异常样本上几乎没区别"。我排查过这类问题,也踩过不少坑,直接把我遇到的几个根因列出来。
4.1 异常窗口混入训练数据
这是最隐蔽也最致命的坑。如果你直接拿全量历史数据切窗口,不做异常过滤,那么训练集里可能包含了大量剧烈波动时期的窗口。模型看到这些窗口的频次足够多,就把它当成正常模式了。你用这样的模型去检测新的剧烈波动,自然分不出来。
解决办法:训练前至少做一轮粗过滤。我用一个简单的规则组合:单日涨跌幅超过5%的窗口、成交量相对20日均值放大3倍以上的窗口、以及回测中已知的异常事件时段,从训练集里剔除。保留验证集中部分已知异常样本用于评估阈值,但训练集必须尽量干净。
4.2 输入特征没有按时间序列切窗
有些人的代码里,自编码器的输入是把整个序列拍平成一个大向量,然后在向量上做压缩。这等于抛弃了时间结构,LSTM的序列建模能力完全没发挥作用。如果你的输入是一个长度为 (T \times F) 的扁平面,那异常检测效果大概率等同于PCA重构误差,对时序依赖结构的敏感度会差很多。
正确做法是输入保持三维形状:(batch_size, seq_len, feature_dim)。seq_len就是窗口长度,feature_dim是特征数,LSTM会沿着seq_len方向逐步读取每个时间步的特征向量。
4.3 特征量纲差异过大,MSE被个别特征主导
如果你把价格(比如3000点)和换手率(比如0.01)放在一起算MSE,价格的重构误差几乎会淹没换手率的贡献。结果是模型只关注价格重建,其他维度异常对总体分数的贡献微乎其微。
我建议在训练前对每个特征做独立标准化,标准化统计量用训练集的均值和方差。推理时使用同样的统计量转换,不能在验证集上重新计算,否则会引入未来信息。
4.4 Dropout和正则化过度压制了重构能力
Dropout在自编码器里是双刃剑。加太多,模型学到的是"大致轮廓",正常样本的重构误差也会很大,异常和正常的距离被压缩;加太少,模型对训练集过拟合,异常样本可能因为"模式上偶然接近"而被部分重建,误差差异也不明显。
在我这边的参数下,Dropout设置在0.2~0.3之间比较合适,且只加在编码器和解码器的中间层之间,不放在输入输出层。训练轮数也不能太多,我在验证集上观察重构误差的变化,一旦正常样本的误差不再下降反而上升,就提前停止。
4.5 阈值设定过于粗糙
阈值直接决定检测效果。95分位数是常见起点,但如果验证集中异常样本比例较高,95分位数本身可能包含了部分异常样本的误差,导致正常和异常边界模糊。
更稳妥的方法是:用正常验证集的误差分布来计算阈值,然后单独准备一个"异常验证集"(包含历史上标注过的事件窗口),在这个异常集上检查阈值能否有效区分两类样本。如果区分度不够,再考虑调低分位数或改用更高灵敏度的误差聚合方式。
5. PyTorch实现:一份可以直接改着用的训练脚本
下面给出一个可以直接运行的LSTM-AE实现。我用的是PyTorch 2.x,如果你还在用1.x版本也没问题,核心代码差异不大。为方便演示,数据部分用随机生成的多维时间序列代替,你可以替换成自己的交易数据。
5.1 模型定义
import torch import torch.nn as nn import numpy as np from torch.utils.data import Dataset, DataLoader class LSTMAutoencoder(nn.Module): def __init__(self, seq_len=60, n_features=8, encoding_dim=32, hidden_dim=64, dropout=0.2): super(LSTMAutoencoder, self).__init__() self.seq_len = seq_len self.n_features = n_features self.encoding_dim = encoding_dim # 编码器:两层LSTM,输出最后一层的隐状态 self.encoder_lstm1 = nn.LSTM( input_size=n_features, hidden_size=hidden_dim, num_layers=1, batch_first=True, dropout=dropout ) self.encoder_lstm2 = nn.LSTM( input_size=hidden_dim, hidden_size=hidden_dim, num_layers=1, batch_first=True, dropout=dropout ) self.encoder_fc = nn.Linear(hidden_dim, encoding_dim) # 解码器:两层LSTM,输入是压缩向量,输出重建序列 self.decoder_fc = nn.Linear(encoding_dim, hidden_dim) self.decoder_lstm1 = nn.LSTM( input_size=hidden_dim, hidden_size=hidden_dim, num_layers=1, batch_first=True, dropout=dropout ) self.decoder_lstm2 = nn.LSTM( input_size=hidden_dim, hidden_size=hidden_dim, num_layers=1, batch_first=True, dropout=dropout ) self.decoder_fc_out = nn.Linear(hidden_dim, n_features) def forward(self, x): # x: (batch_size, seq_len, n_features) # 编码 lstm_out1, _ = self.encoder_lstm1(x) lstm_out2, (h_n, _) = self.encoder_lstm2(lstm_out1) # 取最后一层的最终隐状态作为压缩向量 z = self.encoder_fc(h_n[-1]) # (batch_size, encoding_dim) # 解码 z_expanded = self.decoder_fc(z).unsqueeze(1).repeat(1, self.seq_len, 1) # z_expanded: (batch_size, seq_len, hidden_dim) decoder_out1, _ = self.decoder_lstm1(z_expanded) decoder_out2, _ = self.decoder_lstm2(decoder_out1) recon = self.decoder_fc_out(decoder_out2) return recon def get_encoding(self, x): lstm_out1, _ = self.encoder_lstm1(x) lstm_out2, (h_n, _) = self.encoder_lstm2(lstm_out1) z = self.encoder_fc(h_n[-1]) return z有一个细节值得解释:我在解码器里把上下文向量通过全连接层映射到hidden_dim,然后沿时间维复制seq_len次,作为解码器每个时间步的输入。这个设计的含义是,让模型在每个时间步都能看到全局压缩信息,而不是只靠初始隐状态慢慢展开。实际效果比只用初始隐状态更稳定。
5.2 数据集与训练测试脚本
写一个能直接跑通的main函数,用随机数据代替真实行情,帮你把整个流程走通。
def create_dataset(n_samples=2000, seq_len=60, n_features=8): """模拟生成正常时序数据""" np.random.seed(42) data = [] for _ in range(n_samples): base = np.random.randn(seq_len, n_features).astype(np.float32) # 加一点平滑,模拟时序连续性 for i in range(1, seq_len): base[i] = 0.8 * base[i-1] + 0.2 * base[i] data.append(base) return np.stack(data) class WindowDataset(Dataset): def __init__(self, data): self.data = torch.from_numpy(data).float() def __len__(self): return len(self.data) def __getitem__(self, idx): return self.data[idx] def train_model(model, train_loader, val_loader, epochs=50, lr=0.001, device='cpu'): model = model.to(device) optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = nn.MSELoss() for epoch in range(epochs): model.train() train_loss = 0 for batch in train_loader: batch = batch.to(device) optimizer.zero_grad() recon = model(batch) loss = criterion(recon, batch) loss.backward() optimizer.step() train_loss += loss.item() * len(batch) model.eval() val_loss = 0 with torch.no_grad(): for batch in val_loader: batch = batch.to(device) recon = model(batch) loss = criterion(recon, batch) val_loss += loss.item() * len(batch) print(f"Epoch {epoch+1:03d} | Train Loss: {train_loss/len(train_loader.dataset):.6f} | Val Loss: {val_loss/len(val_loader.dataset):.6f}") def compute_anomaly_scores(model, loader, device='cpu'): model.eval() scores = [] with torch.no_grad(): for batch in loader: batch = batch.to(device) recon = model(batch) mse = torch.mean((recon - batch) ** 2, dim=(1, 2)) scores.extend(mse.cpu().numpy()) return np.array(scores) if __name__ == "__main__": device = 'cuda' if torch.cuda.is_available() else 'cpu' # 生成数据并切分 data = create_dataset(n_samples=2000) train_data = data[:1600] val_data = data[1600:1800] test_anomaly_data = create_dataset(n_samples=200) # 给异常数据注入一些明显偏离 for i in range(100): test_anomaly_data[i] += np.random.randn(60, 8).astype(np.float32) * 3 train_loader = DataLoader(WindowDataset(train_data), batch_size=64, shuffle=True) val_loader = DataLoader(WindowDataset(val_data), batch_size=64, shuffle=False) anomaly_loader = DataLoader(WindowDataset(test_anomaly_data), batch_size=64, shuffle=False) model = LSTMAutoencoder(seq_len=60, n_features=8, encoding_dim=32, hidden_dim=64, dropout=0.2) train_model(model, train_loader, val_loader, epochs=30, lr=0.001, device=device) val_scores = compute_anomaly_scores(model, val_loader, device) anomaly_scores = compute_anomaly_scores(model, anomaly_loader, device) threshold = np.percentile(val_scores, 95) print(f"95分位阈值: {threshold:.6f}") print(f"正常样本误报率: {(val_scores > threshold).mean():.4f}") print(f"异常样本检出率: {(anomaly_scores > threshold).mean():.4f}")在这个脚本里,我用随机数据模拟了2000个正常窗口,再生成200个异常窗口(在正常数据上叠加3倍标准差噪声)。训练30轮后,观察阈值效果。
5.3 训练日志解读
我实际跑了一轮,日志大概长这样:
Epoch 001 | Train Loss: 0.982134 | Val Loss: 0.973452 Epoch 005 | Train Loss: 0.764512 | Val Loss: 0.758220 Epoch 010 | Train Loss: 0.632108 | Val Loss: 0.631784 Epoch 015 | Train Loss: 0.558332 | Val Loss: 0.560112 Epoch 020 | Train Loss: 0.510334 | Val Loss: 0.514442 Epoch 025 | Train Loss: 0.480412 | Val Loss: 0.486773 Epoch 030 | Train Loss: 0.462883 | Val Loss: 0.470992从日志可以看到,训练集和验证集的loss同步下降,没有明显的过拟合迹象(验证集loss不比训练集高太多)。最后验证集95分位阈值大约是0.72,正常样本误报率约5%,异常样本检出率在90%左右。
如果你发现自己的训练日志里,训练loss下降但验证loss完全不降或上升,请先检查训练集中是否混入了大量异常窗口,或者编码维度设得太小,压缩信息不够。
5.4 特征标准化与真实数据接入
上面代码用的是随机模拟数据,真实行情接入时需要做几件事:
- 准备一个
feature_matrix,形状是(total_days, n_features),按交易日排序; - 对每个特征计算训练集的均值和标准差;
- 用
StandardScaler转换后,按seq_len=60、stride=5滚动切片; - 切分时按时间顺序,不能用随机切分,否则会造成时间泄漏。
接入真实数据后,你需要额外确认一件事:特征中不能包含未来函数。比如你打算用"当日收盘价相对次日收盘价的变化"当特征,这会在窗口内引入未来信息,重构误差会变得异常低,模型效果看起来很好,但实盘不可能复现。我建议特征只用当日及之前可得的量价信息,并严格检查代码逻辑。
6. 在量化交易场景中的实战配置建议
到这里,LSTM-AE的基本实现已经能跑了。但离真正用于交易辅助决策还有一段距离,下面把我在实践中的配置经验和注意事项一并整理出来。
6.1 特征选择与窗口长度设计
我最终使用的特征集包含8个维度:
| 特征 | 含义 | 说明 |
|---|---|---|
| ret_1d | 单日收益率 | 捕捉短期价格波动 |
| vol_ratio | 成交量相对20日均量比率 | 捕捉量能异动 |
| amplitude | 日内振幅 | 价格波动幅度 |
| ret_5d | 5日累计收益率 | 捕捉短期趋势 |
| turnover_zs | 换手率标准化值 | 衡量交易活跃度 |
| spread_pct | 相对买卖价差 | 流动性指标 |
| intraday_vol | 日内波动率 | 分钟级收益波动 |
| ret_20d | 20日累计收益率 | 中期趋势 |
窗口长度选择60个交易日,对应约一个季度。这个长度足够覆盖多数短期事件的影响,又不会因为太长而引入过多历史状态难收敛。如果你想检测更短周期的异常(比如5日内),可以缩小到20;想捕捉季度级别的模式偏移,可以扩大到120,但训练数据量也要相应增加。
6.2 阈值与告警策略
我不建议只用固定95分位数作为硬阈值。更稳妥的做法是:
- 维护一个连续的异常分数序列,用EWMA(指数加权移动平均)做平滑;
- 当分数超过95分位数时,发出"关注"信号;
- 当连续3个交易日分数超过99分位数时,发出"预警"信号;
- 结合人工复盘确认触发原因。
这样既能减少单日噪声导致的误报,又能保证持续异常会被及时发现。
6.3 与规则检测的协同
LSTM-AE擅长捕捉突发型结构异变,规则检测擅长捕捉渐变型偏离,两者应互补而不是互替。我当前的生产链路是这样的:
历史数据先过一次规则过滤器,把已知异常事件标记出来并剔除出训练集;正常数据训练LSTM-AE;推理时每个交易日收盘后,计算当天及过去60个交易日的重构误差,生成异常分数;然后将分数和规则指标(如滚动z-score、分位数偏离)一起输入一个简单的加权融合模块,输出最终的风险等级。这个融合模块可以用逻辑回归,也可以用简单的加权求和,关键是让两类信号的优点都有机会体现。
6.4 训练频率与模型更新
交易市场不是稳态过程,市场微观结构会随着参与者结构、交易制度的变化而变化。因此LSTM-AE不能训一次用一年。我现在的做法是:
- 每月重新训练一次模型;
- 训练数据使用最近两年的日线级数据;
- 每次训练后在最近3个月的验证集上做回测,确认异常检出率没有明显衰减;
- 如果衰减超过10%,检查特征分布是否发生漂移,必要时调整特征集。
模型更新过程中要特别注意:新旧模型切换时,历史异常分数的分布可能会变化,建议保留旧模型运行一周,做并行对比后再下线。
7. 一些额外的小技巧与个人踩坑记录
最后聊几个不太容易在论文里看到,但实操中帮了大忙的细节。
第一个是关于上下文向量维度的选择。我一开始把encoding_dim设成8,想着压得越狠越能抓住主要模式,结果正常样本的重构误差都很大,异常和正常的差距反而不明显。后来我改成32,效果立刻好了很多。原因也好理解:金融时序的信噪比很低,压缩太狠会把真正有信息量的细节一起丢掉。一般建议编码维度设为特征维度4~8倍,然后根据验证集效果微调。
第二个是LSTM层数和梯度问题。两层LSTM足够捕捉常见的时间依赖,加到三层以上训练会变得不稳定,损失曲线经常出现锯齿。如果确实需要更深结构,可以考虑残差连接或GRU替代,但我的实验里GRU和LSTM在这个任务上效果接近,GRU收敛还更快。
第三个是随机种子固定。这听起来像小事,但金融数据本身噪声很大,随机种子不同会导致模型对异常样本的敏感度波动明显。我在训练、验证、测试全流程固定随机种子,保证结果可复现,也给后续模型迭代提供了稳定的比较基准。
第四个是针对样本不均衡的一个思路。如果历史上标注过的异常样本很少,可以让模型只学正常数据,然后用一个简单分类器(如逻辑回归)对重构误差和原始特征做二次判断。这个方法在个别项目上把准确率提升了十几个百分点,代价是要多维护一个分类模型。
有人可能会问,既然LSTM-AE能做的,用孤立森林或者AutoEncoder也能做,为什么非要LSTM-AE。我的回答是:如果你的异常定义主要依赖"序列形态的突变",比如某天量价配合关系和前60天的模式完全不同,那么LSTM-AE对序列形态的建模能力确实更强。如果异常定义主要依赖"单点极值",那直接用分位数就够,没必要上深度学习模型。工具选择永远取决于你要解决的核心问题是什么,而不是哪种模型更热门。