第22章:深度学习进阶:使用Autoencoder进行无监督异常检测

波动率曲面的异常检测,说实话,一直是个让人头疼的问题。

传统的统计方法,比如3-sigma、Z-score,在曲面数据上经常失灵。为什么?因为曲面数据太复杂了——它不是一维的,而是包含期限、行权价、波动率三个维度,而且它们之间还有复杂的非线性关系。我刚开始做这块时,用传统方法检测异常,结果误报率高达40%,搞得风控同事天天找我喝茶。

后来我转向了深度学习。Autoencoder,这个工具,说白了就是为无监督异常检测量身定做的。

22.1 为什么是Autoencoder?

先说说核心思路。

Autoencoder的结构很简单:一个编码器,一个解码器。编码器把输入数据压缩成低维表示,解码器再把它还原回来。训练的目标就是让输出尽可能接近输入。

那异常检测怎么用?

你想啊,模型只见过正常数据。它学会了正常数据的模式。如果给它一个异常数据,它还原出来的结果就会和原始输入差很多。这个差异,就是异常分数。

我在项目中遇到过一个问题:有些异常点其实很隐蔽,肉眼根本看不出来。比如某个期限的波动率突然高了0.5个vol,但整体曲面看起来还挺平滑。传统方法直接漏掉了。Autoencoder却能捕捉到这种细微的异常,因为它学的是整个曲面的联合分布。

核心思想:正常数据 → 重建误差小;异常数据 → 重建误差大。

22.2 数据预处理:曲面标准化

做Autoencoder之前,数据预处理是关键一步。我个人习惯分三步走:

  1. 缺失值处理:波动率曲面经常有缺失值,尤其是深度虚值期权。我一般用插值法填充,或者直接剔除。
  2. 标准化:不同期限、不同行权价的波动率数值差异很大。不标准化的话,模型会偏向数值大的维度。
  3. 形状保持:曲面数据是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 异常检测流程

模型训练好之后,异常检测的流程是这样的:

  1. 输入新的曲面数据
  2. 用训练好的Autoencoder重建
  3. 计算每个样本的重建误差(MSE)
  4. 设定阈值,超过阈值的标记为异常

阈值怎么定?我一般用训练集重建误差的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的核心逻辑:

Autoencoder异常检测流程图 原始曲面 编码器 降维压缩 100→16 瓶颈层 16维 解码器 升维还原 16→100 重建曲面 异常判定 原始曲面 vs 重建曲面 计算MSE重建误差 MSE < 阈值 → 正常 MSE > 阈值 → 异常

你看,整个流程其实很直观。模型先压缩,再还原。如果还原出来的和原始的不一样,那就说明这个数据点不太对劲。

22.7 实战中的坑与对策

做了这么多项目,我总结了几条经验:

常见问题 原因 解决方案
重建误差普遍偏大 模型容量不够,学不到曲面特征 增加隐藏层节点数或层数
异常检测准确率低 训练数据本身包含异常 先用规则过滤明显异常,再训练
模型对微小变化不敏感 损失函数权重设置不合理 使用加权MSE,重点区域加大权重
训练不收敛 学习率太大或数据未标准化 降低学习率,检查数据预处理

我的经验:Autoencoder不是万能的。如果曲面数据本身噪声很大,模型会把噪声也学进去。这时候可以先做一次平滑预处理,或者用去噪Autoencoder(DAE)。

22.8 小结

Autoencoder做异常检测,说白了就是让模型学会「什么是正常的」。它不需要标注数据,这在金融领域太实用了——你想想看,我们哪有那么多标注好的异常样本?

我个人觉得,Autoencoder最大的价值在于:它能捕捉到人眼看不到的异常模式。那些微小的、局部的、非线性的异常,传统方法很难发现,但深度学习可以。

当然,它也有局限。模型训练需要一定量的正常数据,而且对超参数比较敏感。但只要你掌握了今天讲的这些要点——数据预处理、架构设计、加权损失、阈值设定——就能在实际项目中用起来。

嗯,今天就到这里。记住一句话:异常检测不是找到所有异常,而是找到值得关注的异常。


公众号:蓝海资料掘金营,微信deep3321