第22章:深度学习进阶:使用Autoencoder进行无监督异常检测
波动率曲面的异常检测,说实话,一直是个让人头疼的问题。
传统的统计方法,比如3-sigma、Z-score,在曲面数据上经常失灵。为什么?因为曲面数据太复杂了——它不是一维的,而是包含期限、行权价、波动率三个维度,而且它们之间还有复杂的非线性关系。我刚开始做这块时,用传统方法检测异常,结果误报率高达40%,搞得风控同事天天找我喝茶。
后来我转向了深度学习。Autoencoder,这个工具,说白了就是为无监督异常检测量身定做的。
22.1 为什么是Autoencoder?
先说说核心思路。
Autoencoder的结构很简单:一个编码器,一个解码器。编码器把输入数据压缩成低维表示,解码器再把它还原回来。训练的目标就是让输出尽可能接近输入。
那异常检测怎么用?
你想啊,模型只见过正常数据。它学会了正常数据的模式。如果给它一个异常数据,它还原出来的结果就会和原始输入差很多。这个差异,就是异常分数。
我在项目中遇到过一个问题:有些异常点其实很隐蔽,肉眼根本看不出来。比如某个期限的波动率突然高了0.5个vol,但整体曲面看起来还挺平滑。传统方法直接漏掉了。Autoencoder却能捕捉到这种细微的异常,因为它学的是整个曲面的联合分布。
核心思想:正常数据 → 重建误差小;异常数据 → 重建误差大。
22.2 数据预处理:曲面标准化
做Autoencoder之前,数据预处理是关键一步。我个人习惯分三步走:
- 缺失值处理:波动率曲面经常有缺失值,尤其是深度虚值期权。我一般用插值法填充,或者直接剔除。
- 标准化:不同期限、不同行权价的波动率数值差异很大。不标准化的话,模型会偏向数值大的维度。
- 形状保持:曲面数据是2D矩阵,但神经网络通常吃1D向量。需要把矩阵展平,但要记住原始形状,方便后续可视化。
import numpy as np
from sklearn.preprocessing import StandardScaler
def preprocess_surface(surface_data):
"""
surface_data: shape (n_samples, n_tenors, n_strikes)
"""
n_samples, n_tenors, n_strikes = surface_data.shape
# 展平
flat_data = surface_data.reshape(n_samples, -1)
# 标准化
scaler = StandardScaler()
normalized = scaler.fit_transform(flat_data)
return normalized, scaler
小技巧:标准化时,我建议按每个时间点单独做,而不是全局标准化。因为不同时间点的波动率水平可能差异很大,全局标准化会抹掉这种时间序列特征。
22.3 模型架构设计
Autoencoder的架构,说白了就是编码器+解码器。但具体怎么设计,有讲究。
我常用的架构是这样的:
- 输入层:曲面展平后的维度,比如10个期限×10个行权价=100维
- 编码器:3层全连接,逐层降维:100 → 64 → 32 → 16
- 瓶颈层:16维,这是压缩后的表示
- 解码器:对称结构,逐层升维:16 → 32 → 64 → 100
- 输出层:和输入维度相同,激活函数用linear
嗯,这里要注意:瓶颈层的维度不能太小,否则信息丢失太多;也不能太大,否则模型会直接记住输入,失去泛化能力。我一般取输入维度的1/5到1/10。
import torch
import torch.nn as nn
class VolSurfaceAutoencoder(nn.Module):
def __init__(self, input_dim, encoding_dim=16):
super().__init__()
# 编码器
self.encoder = nn.Sequential(
nn.Linear(input_dim, 64),
nn.ReLU(),
nn.Linear(64, 32),
nn.ReLU(),
nn.Linear(32, encoding_dim),
nn.ReLU()
)
# 解码器
self.decoder = nn.Sequential(
nn.Linear(encoding_dim, 32),
nn.ReLU(),
nn.Linear(32, 64),
nn.ReLU(),
nn.Linear(64, input_dim)
# 注意:输出层不用激活函数
)
def forward(self, x):
encoded = self.encoder(x)
decoded = self.decoder(encoded)
return decoded
22.4 训练策略与损失函数
训练Autoencoder,损失函数一般用MSE(均方误差)。但我在实践中发现,直接用MSE有个问题:它对所有维度的误差一视同仁。
实际上,波动率曲面不同位置的敏感度是不一样的。ATM附近的波动率,交易员盯得最紧,误差应该惩罚更重。OTM和ITM的波动率,相对宽松一些。
所以我建议用加权MSE:
def weighted_mse_loss(input, target, weights):
"""
weights: 每个维度的权重,形状和input相同
"""
diff = (input - target) ** 2
weighted_diff = diff * weights
return weighted_diff.mean()
权重怎么设?我一般根据行权价距离ATM的远近,给一个高斯衰减权重。ATM附近权重最高,越往两边越低。
避坑指南:我曾经在训练时忘记加正则化,结果模型过拟合严重。在验证集上重建误差很小,但一遇到新数据就崩。建议加L2正则化或者Dropout。
22.5 异常检测流程
模型训练好之后,异常检测的流程是这样的:
- 输入新的曲面数据
- 用训练好的Autoencoder重建
- 计算每个样本的重建误差(MSE)
- 设定阈值,超过阈值的标记为异常
阈值怎么定?我一般用训练集重建误差的95分位数。也可以根据业务需求调整——风控严格的话,用90分位数;宽松的话,用99分位数。
def detect_anomalies(model, data, threshold_percentile=95):
model.eval()
with torch.no_grad():
reconstructed = model(data)
mse = torch.mean((data - reconstructed) ** 2, dim=1)
threshold = torch.quantile(mse, threshold_percentile / 100.0)
anomalies = mse > threshold
return anomalies.numpy(), mse.numpy(), threshold.item()
22.6 可视化:看看模型学到了什么
光看数字不够直观。我习惯把重建结果和原始数据放在一起对比,看看模型到底学到了什么。
下面这张图展示了Autoencoder的核心逻辑:
你看,整个流程其实很直观。模型先压缩,再还原。如果还原出来的和原始的不一样,那就说明这个数据点不太对劲。
22.7 实战中的坑与对策
做了这么多项目,我总结了几条经验:
| 常见问题 | 原因 | 解决方案 |
|---|---|---|
| 重建误差普遍偏大 | 模型容量不够,学不到曲面特征 | 增加隐藏层节点数或层数 |
| 异常检测准确率低 | 训练数据本身包含异常 | 先用规则过滤明显异常,再训练 |
| 模型对微小变化不敏感 | 损失函数权重设置不合理 | 使用加权MSE,重点区域加大权重 |
| 训练不收敛 | 学习率太大或数据未标准化 | 降低学习率,检查数据预处理 |
我的经验:Autoencoder不是万能的。如果曲面数据本身噪声很大,模型会把噪声也学进去。这时候可以先做一次平滑预处理,或者用去噪Autoencoder(DAE)。
22.8 小结
Autoencoder做异常检测,说白了就是让模型学会「什么是正常的」。它不需要标注数据,这在金融领域太实用了——你想想看,我们哪有那么多标注好的异常样本?
我个人觉得,Autoencoder最大的价值在于:它能捕捉到人眼看不到的异常模式。那些微小的、局部的、非线性的异常,传统方法很难发现,但深度学习可以。
当然,它也有局限。模型训练需要一定量的正常数据,而且对超参数比较敏感。但只要你掌握了今天讲的这些要点——数据预处理、架构设计、加权损失、阈值设定——就能在实际项目中用起来。
嗯,今天就到这里。记住一句话:异常检测不是找到所有异常,而是找到值得关注的异常。
公众号:蓝海资料掘金营,微信deep3321